diff --git a/bundle/nvim-cmp/.github/FUNDING.yml b/bundle/nvim-cmp/.github/FUNDING.yml deleted file mode 100644 index ccdeccc61..000000000 --- a/bundle/nvim-cmp/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -# These are supported funding model platforms - -github: [hrsh7th] diff --git a/bundle/nvim-cmp/.github/ISSUE_TEMPLATE/bug_report.yml b/bundle/nvim-cmp/.github/ISSUE_TEMPLATE/bug_report.yml index 2b24357df..4fc4a2c5f 100644 --- a/bundle/nvim-cmp/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/bundle/nvim-cmp/.github/ISSUE_TEMPLATE/bug_report.yml @@ -7,24 +7,17 @@ body: attributes: label: FAQ options: - - label: I have checked the [FAQ](https://github.com/hrsh7th/nvim-cmp/blob/15f08a8faa22d52480cdcb9ef9ca698120f04363/doc/cmp.txt#L616) and it didn't resolve my problem. + - label: I have checked the [FAQ](https://github.com/hrsh7th/nvim-cmp/blob/main/doc/cmp.txt) and it didn't resolve my problem. required: true - type: checkboxes - id: issue-prerequisite + id: announcement-prerequisite attributes: - label: Issues + label: Announcement options: - - label: I have checked [existing issues](https://github.com/hrsh7th/nvim-cmp/issues) and there are no open or closed issues with the same problem. + - label: I have checked [Breaking change announcement](https://github.com/hrsh7th/nvim-cmp/issues/231). required: true - - type: input - attributes: - label: "Neovim Version" - description: "`nvim --version`:" - validations: - required: true - - type: textarea attributes: label: "Minimal reproducible full config" @@ -34,6 +27,10 @@ body: 2. Edit `~/cmp-repro.vim` for reproducing the issue 3. Open `nvim -u ~/cmp-repro.vim` 4. Check reproduction step + value: | + ```vim + + ``` validations: required: true diff --git a/bundle/nvim-cmp/.github/workflows/format.yaml b/bundle/nvim-cmp/.github/workflows/format.yaml new file mode 100644 index 000000000..8e5c4a293 --- /dev/null +++ b/bundle/nvim-cmp/.github/workflows/format.yaml @@ -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" diff --git a/bundle/nvim-cmp/.github/workflows/integration.yaml b/bundle/nvim-cmp/.github/workflows/integration.yaml index 4bac4baca..3bf658fa0 100644 --- a/bundle/nvim-cmp/.github/workflows/integration.yaml +++ b/bundle/nvim-cmp/.github/workflows/integration.yaml @@ -16,13 +16,6 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Setup rust - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - default: true - override: true - - name: Setup neovim uses: rhysd/action-setup-vim@v1 with: @@ -40,12 +33,9 @@ jobs: - name: Setup tools shell: bash run: | - sudo apt install -y curl unzip --no-install-recommends - bash ./utils/install_stylua.sh luarocks install luacheck luarocks install vusted - name: Run tests shell: bash run: make integration - diff --git a/bundle/nvim-cmp/.github/workflows/release.yaml b/bundle/nvim-cmp/.github/workflows/release.yaml new file mode 100644 index 000000000..1acd349ce --- /dev/null +++ b/bundle/nvim-cmp/.github/workflows/release.yaml @@ -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". + diff --git a/bundle/nvim-cmp/Makefile b/bundle/nvim-cmp/Makefile index 731b3f283..d108f32ef 100644 --- a/bundle/nvim-cmp/Makefile +++ b/bundle/nvim-cmp/Makefile @@ -1,7 +1,3 @@ -.PHONY: fmt -fmt: - stylua --config-path stylua.toml --glob 'lua/**/*.lua' -- lua - .PHONY: lint lint: luacheck ./lua @@ -12,13 +8,10 @@ test: .PHONY: pre-commit pre-commit: - ./utils/stylua --config-path stylua.toml --glob 'lua/**/*.lua' -- lua luacheck lua vusted lua .PHONY: integration integration: - ./utils/stylua --config-path stylua.toml --check --glob 'lua/**/*.lua' -- lua luacheck lua vusted lua - diff --git a/bundle/nvim-cmp/README.md b/bundle/nvim-cmp/README.md index a096cc458..48a11636c 100644 --- a/bundle/nvim-cmp/README.md +++ b/bundle/nvim-cmp/README.md @@ -3,34 +3,31 @@ A completion engine plugin for neovim written in Lua. Completion sources are installed from external repositories and "sourced". - +https://github.com/hrsh7th/nvim-cmp/assets/22756295/afa70011-9121-4e42-aedd-0153b630eeab Readme! ==================== -1. nvim-cmp's breaking changes are documented [here](https://github.com/hrsh7th/nvim-cmp/issues/231). +1. There is a GitHub issue that documents [breaking changes](https://github.com/hrsh7th/nvim-cmp/issues/231) for nvim-cmp. Subscribe to the issue to be notified of upcoming breaking changes. 2. This is my hobby project. You can support me via GitHub sponsors. -3. Bug reports are welcome, but I might not fix if you don't provide a minimal reproduction configuration and steps. -4. The nvim-cmp documents is [here](./doc/cmp.txt). - - +3. Bug reports are welcome, but don't expect a fix unless you provide minimal configuration and steps to reproduce your issue. +4. The `cmp.mapping.preset.*` is pre-defined configuration that aims to mimic neovim's native like behavior. It can be changed without announcement. Please manage key-mapping by yourself. Concept ==================== - Full support for LSP completion related capabilities - Powerful customizability via Lua functions -- Smart handling of key mapping +- Smart handling of key mappings - No flicker - Setup ==================== ### Recommended Configuration -This example configuration uses `vim-plug` as the plugin manager and `vim-vsnip` as snippet plugin. +This example configuration uses `vim-plug` as the plugin manager and `vim-vsnip` as a snippet plugin. ```lua call plug#begin(s:plug_dir) @@ -59,10 +56,8 @@ Plug 'hrsh7th/vim-vsnip' call plug#end() -set completeopt=menu,menuone,noselect - lua <'] = cmp.mapping(cmp.mapping.scroll_docs(-4), { 'i', 'c' }), - [''] = cmp.mapping(cmp.mapping.scroll_docs(4), { 'i', 'c' }), - [''] = cmp.mapping(cmp.mapping.complete(), { 'i', 'c' }), - [''] = cmp.config.disable, -- Specify `cmp.config.disable` if you want to remove the default `` mapping. - [''] = cmp.mapping({ - i = cmp.mapping.abort(), - c = cmp.mapping.close(), - }), - [''] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items. + window = { + -- completion = cmp.config.window.bordered(), + -- documentation = cmp.config.window.bordered(), }, + mapping = cmp.mapping.preset.insert({ + [''] = cmp.mapping.scroll_docs(-4), + [''] = cmp.mapping.scroll_docs(4), + [''] = cmp.mapping.complete(), + [''] = cmp.mapping.abort(), + [''] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items. + }), sources = cmp.config.sources({ { name = 'nvim_lsp' }, { name = 'vsnip' }, -- For vsnip users. @@ -100,14 +95,15 @@ lua < with each lsp server you've enabled. require('lspconfig')[''].setup { capabilities = capabilities @@ -131,85 +128,12 @@ lua < lua vimrc.cmp.lsp() - inoremap lua vimrc.cmp.snippet() -]]) -``` - -### Full managed completion behavior. - -```lua -local cmp = require('cmp') - -cmp.setup { - completion = { - autocomplete = false, -- disable auto-completion. - } -} - -_G.vimrc = _G.vimrc or {} -_G.vimrc.cmp = _G.vimrc.cmp or {} -_G.vimrc.cmp.on_text_changed = function() - local cursor = vim.api.nvim_win_get_cursor(0) - local line = vim.api.nvim_get_current_line() - local before = string.sub(line, 1, cursor[2] + 1) - if before:match('%s*$') then - cmp.complete() -- Trigger completion only if the cursor is placed at the end of line. - end -end -vim.cmd([[ - augroup vimrc - autocmd - autocmd TextChanged,TextChangedI,TextChangedP * call luaeval('vimrc.cmp.on_text_changed()') - augroup END -]]) -``` - - - +See the [Wiki](https://github.com/hrsh7th/nvim-cmp/wiki). diff --git a/bundle/nvim-cmp/autoload/cmp.vim b/bundle/nvim-cmp/autoload/cmp.vim index 43b8cc282..a331d0716 100644 --- a/bundle/nvim-cmp/autoload/cmp.vim +++ b/bundle/nvim-cmp/autoload/cmp.vim @@ -6,7 +6,16 @@ let s:sources = {} " function! cmp#register_source(name, source) abort let l:methods = [] - for l:method in ['is_available', 'get_debug_name', 'get_trigger_characters', 'get_keyword_pattern', 'complete', 'execute', 'resolve'] + for l:method in [ + \ 'is_available', + \ 'get_debug_name', + \ 'get_position_encoding_kind', + \ 'get_trigger_characters', + \ 'get_keyword_pattern', + \ 'complete', + \ 'execute', + \ 'resolve' + \ ] if has_key(a:source, l:method) && type(a:source[l:method]) == v:t_func call add(l:methods, l:method) endif @@ -39,6 +48,8 @@ function! cmp#_method(bridge_id, method, args) abort return l:source[a:method]() elseif a:method ==# 'get_debug_name' return l:source[a:method]() + elseif a:method ==# 'get_position_encoding_kind' + return l:source[a:method](a:args[0]) elseif a:method ==# 'get_keyword_pattern' return l:source[a:method](a:args[0]) elseif a:method ==# 'get_trigger_characters' diff --git a/bundle/nvim-cmp/doc/cmp.txt b/bundle/nvim-cmp/doc/cmp.txt index bdb12d01d..935d322a8 100644 --- a/bundle/nvim-cmp/doc/cmp.txt +++ b/bundle/nvim-cmp/doc/cmp.txt @@ -12,45 +12,40 @@ Function |cmp-function| Mapping |cmp-mapping| Command |cmp-command| Highlight |cmp-highlight| +FileType |cmp-filetype| Autocmd |cmp-autocmd| Config |cmp-config| +Config Helper |cmp-config-helper| Develop |cmp-develop| FAQ |cmp-faq| - - ============================================================================== Abstract *cmp-abstract* This is nvim-cmp's document. -1. This docs uses the type definition notation like `{lsp,cmp,vim}.*` - - You can find it `../lua/cmp/types/init.lua`. -2. The advanced configuration is noted in wiki. +1. This help file uses the type definition notation like `{lsp,cmp,vim}.*` + - You can find it in `../lua/cmp/types/init.lua`. +2. Advanced configuration is described in the wiki. - https://github.com/hrsh7th/nvim-cmp/wiki - - ============================================================================== Concept *cmp-concept* - Full support for LSP completion related capabilities -- Powerful customizability via Lua functions -- Smart handling of key mapping +- Powerful customization abilities via Lua functions +- Smart handling of key mappings - No flicker - - ============================================================================== Usage *cmp-usage* -The recommendation configurations are the below. - -NOTE: - 1. You must setup `snippet.expand` function. - 2. The `cmp.setup.cmdline` won't work if you are using `native` completion menu. - 3. You can disable the `default` options via specifying `cmp.config.disable` value. -> +A recommended configuration can be found below. + NOTE: + 1. You must provide a `snippet.expand` function. + 2. `cmp.setup.cmdline` won't work if you use the `native` completion menu. + 3. You can disable the `default` options by specifying `cmp.config.disable` value. +>vim call plug#begin(s:plug_dir) Plug 'neovim/nvim-lspconfig' Plug 'hrsh7th/cmp-nvim-lsp' @@ -92,18 +87,16 @@ NOTE: -- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users. end, }, - mapping = { - [''] = cmp.mapping(cmp.mapping.scroll_docs(-4), { 'i', 'c' }), - [''] = cmp.mapping(cmp.mapping.scroll_docs(4), { 'i', 'c' }), - [''] = cmp.mapping(cmp.mapping.complete(), { 'i', 'c' }), - [''] = cmp.mapping({ - i = cmp.mapping.abort(), - c = cmp.mapping.close(), - }), - -- Accept currently selected item. If none selected, `select` first item. - -- Set `select` to `false` to only confirm explicitly selected items. - [''] = cmp.mapping.confirm({ select = true }), + window = { + -- completion = cmp.config.window.bordered(), + -- documentation = cmp.config.window.bordered(), }, + mapping = cmp.mapping.preset.insert({ + [''] = cmp.mapping.scroll_docs(-4), + [''] = cmp.mapping.scroll_docs(4), + [''] = cmp.mapping.complete(), + [''] = cmp.mapping.confirm({ select = true }), + }), sources = cmp.config.sources({ { name = 'nvim_lsp' }, { name = 'vsnip' }, -- For vsnip users. @@ -117,6 +110,7 @@ NOTE: -- `/` cmdline setup. cmp.setup.cmdline('/', { + mapping = cmp.mapping.preset.cmdline(), sources = { { name = 'buffer' } } @@ -124,6 +118,7 @@ NOTE: -- `:` cmdline setup. cmp.setup.cmdline(':', { + mapping = cmp.mapping.preset.cmdline(), sources = cmp.config.sources({ { name = 'path' } }, { @@ -132,65 +127,63 @@ NOTE: }) -- Setup lspconfig. - local capabilities = require('cmp_nvim_lsp').update_capabilities(vim.lsp.protocol.make_client_capabilities()) + local capabilities = require('cmp_nvim_lsp').default_capabilities() require('lspconfig')[%YOUR_LSP_SERVER%].setup { capabilities = capabilities } EOF < - - ============================================================================== Function *cmp-function* -NOTE: You can call these functions in mapping via `lua require('cmp').complete()`. +NOTE: `lua require('cmp').complete()` can be used to call these functions in a mapping. *cmp.setup* (config: cmp.ConfigSchema) - Setup global configuration. See configuration option. + Setup global configuration. See configuration options. *cmp.setup.filetype* (filetype: string, config: cmp.ConfigSchema) - Setup filetype configuration to the specific filetype. + Setup filetype-specific configuration. *cmp.setup.buffer* (config: cmp.ConfigSchema) - Setup buffer configuration to the current buffer. + Setup configuration for the current buffer. *cmp.setup.cmdline* (cmdtype: string, config: cmp.ConfigSchema) - Setup cmdline configuration to the specific cmdtype. - See |getcmdtype()| - NOTE: nvim-cmp does not support the `=` cmdtype. + Setup cmdline configuration for the specific type of command. + See |getcmdtype()|. + NOTE: nvim-cmp does not support the `=` command type. *cmp.visible* () - Return the completion menu is visible or not. + Return a boolean showing whether the completion menu is visible or not. *cmp.get_entries* () - Return current all entries. + Return all current entries. *cmp.get_selected_entry* () - Return current selected entry. (contains preselected) + Return currently selected entry (including preselected). *cmp.get_active_entry* () - Return current selected entry. (without preselected) + Return currently selected entry (excluding preselected). *cmp.close* () - Just close the completion menu. + Close the completion menu. *cmp.abort* () - Closes the completion menu and restore the current line to the state when it was started current completion. + Closes the completion menu and restore the current line to the state before the current completion was started. -*cmp.select_next_item* (option: { behavior = cmp.SelectBehavior }) - Select next item. +*cmp.select_next_item* (option: { behavior = cmp.SelectBehavior, count = 1 }) + Select the next item. Set count with large number to select pagedown. -*cmp.select_prev_item* (option: { behavior = cmp.SelectBehavior })* - Select prev item. +*cmp.select_prev_item* (option: { behavior = cmp.SelectBehavior, count = 1 }) + Select the previous item. Set count with large number to select pageup. *cmp.scroll_docs* (delta: number) - Scroll docs if it visible. + Scroll the documentation window if visible. *cmp.complete* (option: { reason = cmp.ContextReason, config = cmp.ConfigSchema }) Invoke completion. - The following configurations defines the key mapping to invoke only snippet completion. -> + The following configuration defines a key mapping to show completion only for vsnip snippets. +>lua cmp.setup { mapping = { [''] = cmp.mapping.complete({ @@ -202,14 +195,14 @@ NOTE: You can call these functions in mapping via `lua require('cmp').compl }) } } -< > +< >vim inoremap lua require('cmp').complete({ config = { sources = { { name = 'vsnip' } } } }) < - NOTE: The `config` means a temporary setting, but the `config.mapping` remains permanent. + NOTE: `config` in that case means a temporary setting, but `config.mapping` remains permanent. *cmp.complete_common_string* () - Complete common string as like as shell completion behavior. -> + Complete common string (similar to shell completion behavior). +>lua cmp.setup { mapping = { [''] = cmp.mapping(function(fallback) @@ -222,51 +215,81 @@ NOTE: You can call these functions in mapping via `lua require('cmp').compl } < *cmp.confirm* (option: cmp.ConfirmOption, callback: function) - Accept current selected completion item. - If you didn't select any items and specified the `{ select = true }` for - this, nvim-cmp will automatically select the first item. + Accepts the currently selected completion item. + If you didn't select any item and the option table contains `select = true`, + nvim-cmp will automatically select the first item. -*cmp.event:on* ('%EVENT_NAME%, callback) - Subscribe nvim-cmp's events below. + You can control how the completion item is injected into + the file through the `behavior` option: + + `behavior=cmp.ConfirmBehavior.Insert`: inserts the selected item and + moves adjacent text to the right (default). + `behavior=cmp.ConfirmBehavior.Replace`: replaces adjacent text with + the selected item. +>lua + cmp.setup { + mapping = { + [""] = cmp.mapping.confirm({ select = true, behavior = cmp.ConfirmBehavior.Replace }), + } + } +< +*cmp.event:on* (%EVENT_NAME%, callback) + Subscribe to nvim-cmp's event. Events are listed below. - `complete_done`: emit after current completion is done. - `confirm_done`: emit after confirmation is done. - - + - `menu_opened`: emit after opening a new completion menu. Called with a table holding a key + named `window`, pointing to the completion menu implementation. + - `menu_closed`: emit after completion menu is closed. Called with a table holding a key + named `window`, pointing to the completion menu implementation. ============================================================================== Mapping *cmp-mapping* -The nvim-cmp's mapping mechanism is complex but flexible and user-friendly. +Nvim-cmp's mapping mechanism is complex but flexible and user-friendly. -You can specify the mapping as function that receives the `fallback` function as arguments. +You can specify a mapping function that receives a `fallback` function as an argument. The `fallback` function can be used to call an existing mapping. -For example, typical pair-wise plugins automatically defines a mapping for `` or `(`. -The nvim-cmp will overwrite it but you can fallback to the original mapping via invoking the `fallback` function. -> +For example, typical pair-wise plugins automatically define mappings for `` and `(`. +Nvim-cmp will overwrite it if you provide a mapping. To call the existing mapping, +you would need to invoke the `fallback` function. +>lua cmp.setup { mapping = { [''] = function(fallback) if cmp.visible() then cmp.confirm() else - fallback() -- If you are using vim-endwise, this fallback function will be behaive as the vim-endwise. + fallback() -- If you use vim-endwise, this fallback will behave the same as vim-endwise. + end + end + } + } +< >lua + cmp.setup { + mapping = { + [''] = function(fallback) + if cmp.visible() then + cmp.select_next_item() + else + fallback() end end } } < -And you can specify the mapping modes. -> + +It is possible to specify the modes the mapping should be active in (`i` = insert mode, `c` = command mode, `s` = select mode): +>lua cmp.setup { mapping = { [''] = cmp.mapping(your_mapping_function, { 'i', 'c' }) } } < -And you can specify the different mapping function for each modes. -> +You can also specify different mappings for different modes by passing a table: +>lua cmp.setup { mapping = { [''] = cmp.mapping({ @@ -276,34 +299,34 @@ And you can specify the different mapping function for each modes. } } < -You can also use built-in mapping helpers. +There are also builtin mapping helper functions you can use: *cmp.mapping.close* () - Same as |cmp.close| + Same as |cmp.close|. *cmp.mapping.abort* () - Same as |cmp.abort| + Same as |cmp.abort|. - *cmp.mapping.select_next_item* (option: { behavior = cmp.SelectBehavior }) - Same as |cmp.select_next_item| + *cmp.mapping.select_next_item* (option: { behavior = cmp.SelectBehavior, count = 1 }) + Same as |cmp.select_next_item|. - *cmp.mapping.select_prev_item* (option: { behavior = cmp.SelectBehavior }) - Same as |cmp.select_prev_item| + *cmp.mapping.select_prev_item* (option: { behavior = cmp.SelectBehavior, count = 1 }) + Same as |cmp.select_prev_item|. *cmp.mapping.scroll_docs* (delta: number) - Same as |cmp.scroll_docs| + Same as |cmp.scroll_docs|. *cmp.mapping.complete* (option: cmp.CompleteParams) - Same as |cmp.complete| + Same as |cmp.complete|. *cmp.mapping.complete_common_string* () - Same as |cmp.complete_common_string| + Same as |cmp.complete_common_string|. *cmp.mapping.confirm* (option: cmp.ConfirmOption) - Same as |cmp.confirm| + Same as |cmp.confirm|. -The built-in mapping helper is only available as a configuration option. -If you want to call the nvim-cmp features directly, please use |cmp-function| instead. +Built-in mapping helpers are only available as a configuration option. +If you want to call nvim-cmp features directly, please use |cmp-function| instead. @@ -311,9 +334,10 @@ If you want to call the nvim-cmp features directly, please use |cmp-function| in Command *cmp-command* *CmpStatus* - Prints source statuses for the current buffer and states. - Sometimes `unknown` source will be printed but it isn't problem. (e.g. `cmp-nvim-lsp`) - That the reason is the `cmp-nvim-lsp` will registered on the InsertEnter autocmd. + Describes statuses and states of sources. + Sometimes `unknown` will be printed - this is expected. + For example, `cmp-nvim-lsp` registers itself on InsertEnter autocommand + so the status will be shown as `unknown` when running the command. @@ -321,60 +345,98 @@ Command *cmp-command* Highlight *cmp-highlight* *CmpItemAbbr* - The abbr field's highlight group. + Highlight group for unmatched characters of each completion field. *CmpItemAbbrDeprecated* - The abbr field's highlight group that only used for deprecated item. + Highlight group for unmatched characters of each deprecated completion field. *CmpItemAbbrMatch* - The matched character's highlight group. + Highlight group for matched characters of each completion field. Matched characters + must form a substring of a field which share a starting position. *CmpItemAbbrMatchFuzzy* - The fuzzy matched character's highlight group. + Highlight group for fuzzy-matched characters of each completion field. *CmpItemKind* - The kind field's highlight group. + Highlight group for the kind of the field. + +NOTE: `kind` is a symbol after each completion option. *CmpItemKind%KIND_NAME%* - The kind field's highlight group for specific `lsp.CompletionItemKind`. - If you want to overwrite only the method kind's highlight group, you can do this. -> + Highlight group for the kind of the field for a specific `lsp.CompletionItemKind`. + If you only want to overwrite the `method` kind's highlight group, you can do this: +>vim highlight CmpItemKindMethod guibg=NONE guifg=Orange < *CmpItemMenu* The menu field's highlight group. +============================================================================== +FileType *cmp-filetype* +*cmp_menu* + The completion menu buffer's filetype. + +*cmp_docs* + The documentation window buffer's filetype. ============================================================================== Autocmd *cmp-autocmd* You can create custom autocommands for certain nvim-cmp events by defining -autocommands for the User event with the following patterns. +autocommands for the User event with the following patterns: *CmpReady* Invoked when nvim-cmp gets sourced from `plugin/cmp.lua`. - - ============================================================================== Config *cmp-config* -You can specify the following configuration option via `cmp.setup { ... }` call. +You can use the following options via `cmp.setup { ... }` . *cmp-config.enabled* enabled~ `boolean | fun(): boolean` - You can control nvim-cmp should work or not via this option. + Toggles the plugin on and off. + + *cmp-config.performance.debounce* +performance.debounce~ + `number` + Sets debounce time + This is the interval used to group up completions from different sources + for filtering and displaying. + + *cmp-config.performance.throttle* +performance.throttle~ + `number` + Sets throttle time + This is used to delay filtering and displaying completions. + + *cmp-config.performance.fetching_timeout* +performance.fetching_timeout~ + `number` + Sets the timeout of candidate fetching process. + The nvim-cmp will wait to display the most prioritized source. + + *cmp-config.performance.async_budget* +performance.async_budget~ + `number` + Maximum time (in ms) an async function is allowed to run during + one step of the event loop. + + *cmp-config.performance.max_view_entries* +performance.max_view_entries~ + `number` + Maximum number of items to show in the entries list. *cmp-config.preselect* preselect~ `cmp.PreselectMode` 1. `cmp.PreselectMode.Item` - nvim-cmp will pre-select the item that the source specified. + nvim-cmp will preselect the item that the source specified. 2. `cmp.PreselectMode.None` - nvim-cmp wouldn't pre-select any item. + nvim-cmp will not preselect any items. *cmp-config.mapping* mapping~ @@ -384,7 +446,8 @@ mapping~ *cmp-config.snippet.expand* snippet.expand~ `fun(option: cmp.SnippetExpansionParams)` - The snippet expansion function. You must integrate your snippet engine plugin via this. + The snippet expansion function. That's how nvim-cmp interacts with a + particular snippet engine. *cmp-config.completion.keyword_length* completion.keyword_length~ @@ -399,51 +462,70 @@ completion.keyword_pattern~ *cmp-config.completion.autocomplete* completion.autocomplete~ `cmp.TriggerEvent[] | false` - The auto-completion trigger events. If you specify this value to false, the - nvim-cmp does not completion automatically but you can still use the manual - completion though. + The event to trigger autocompletion. If set to `false`, then completion is + only invoked manually (e.g. by calling `cmp.complete`). *cmp-config.completion.completeopt* completion.completeopt~ `string` - The vim's completeopt like setting. See 'completeopt'. - Besically, You don't need to modify this. + Like vim's completeopt setting. See 'completeopt'. + In general, you don't need to change this. + + *cmp-config.confirmation.get_commit_characters* +confirmation.get_commit_characters~ + `fun(commit_characters:string[]):string[]` + You can append or exclude commitCharacters via this configuration option + function. The commitCharacters are defined by the LSP spec. + + *cmp-config.formatting.expandable_indicator* +formatting.expandable_indicator~ + `cmp.expandable_indicator` + Boolean to show the `~` expandable indicator in cmp's floating window. *cmp-config.formatting.fields* formatting.fields~ `cmp.ItemField[]` - The array of completion menu field to specify the order of them. + An array of completion fields to specify their order. *cmp-config.formatting.format* formatting.format~ `fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem` - The function to customize the completion menu appearance. See |complete-items|. - This value also can be used to modify `dup` property. - NOTE: The `vim.CompletedItem` can have special properties `abbr_hl_group`, - `kind_hl_group` and `menu_hl_group`. + The function used to customize the appearance of the completion menu. See + |complete-items|. This value can also be used to modify the `dup` property. + NOTE: The `vim.CompletedItem` can contain the special properties + `abbr_hl_group`, `kind_hl_group` and `menu_hl_group`. *cmp-config.matching.disallow_fuzzy_matching* matching.disallow_fuzzy_matching~ `boolean` - Specify disallow or allow fuzzy matching. + Whether to allow fuzzy matching. + *cmp-config.matching.disallow_fullfuzzy_matching* +matching.disallow_fullfuzzy_matching~ + `boolean` + Whether to allow full-fuzzy matching. + + *cmp-config.matching.disallow_partial_fuzzy_matching* +matching.disallow_partial_fuzzy_matching~ + `boolean` + Whether to allow fuzzy matching without prefix matching. *cmp-config.matching.disallow_partial_matching* matching.disallow_partial_matching~ `boolean` - Specify disallow or allow partial matching. + Whether to allow partial matching. *cmp-config.matching.disallow_prefix_unmatching* matching.disallow_prefix_unmatching~ `boolean` - Specify disallow or allow prefix unmatching. + Whether to allow prefix unmatching. *cmp-config.sorting.priority_weight* sorting.priority_weight~ `number` - Specifically, each item's original priority (given by its corresponding source) will be - increased by `#sources - (source_index - 1)` multiplied by `priority_weight`. + Each item's original priority (given by its corresponding source) will be + increased by `#sources - (source_index - 1)` and multiplied by `priority_weight`. That is, the final priority is calculated by the following formula: -> +>lua final_score = orig_score + ((#sources - (source_index - 1)) * sorting.priority_weight) < *cmp-config.sorting.comparators* @@ -455,52 +537,47 @@ sorting.comparators~ *cmp-config.sources* sources~ `cmp.SourceConfig[]` - Array of the source configuration to use. - The order will be used to the completion menu's sort order. + List of the sources and their configurations to use. + The order of the sources determines their order in the completion results. *cmp-config.sources[n].name* sources[n].name~ `string` - The source name. + The name of the source. *cmp-config.sources[n].option* sources[n].option~ `table` - The source specific custom option that defined by the source. + Any specific options defined by the source itself. *cmp-config.sources[n].keyword_length* sources[n].keyword_length~ `number` - The source specific keyword length to trigger auto completion. + The source-specific keyword length to trigger auto completion. *cmp-config.sources[n].keyword_pattern* sources[n].keyword_pattern~ - `number` - The source specific keyword pattern. + `string` + The source-specific keyword pattern. *cmp-config.sources[n].trigger_characters* sources[n].trigger_characters~ `string[]` - The source specific keyword pattern. + A source-specific keyword pattern. *cmp-config.sources[n].priority* sources[n].priority~ `number` - The source specific priority value. - - *cmp-config.sources[n].max_item_count* -sources[n].max_item_count~ - `number` - The source specific item count. + The source-specific priority value. *cmp-config.sources[n].group_index* sources[n].group_index~ `number` The source group index. - For example, You can specify the `buffer` source group index to bigger number - if you don't want to see the buffer source items when the nvim-lsp source is available. -> + For instance, you can set the `buffer`'s source `group_index` to a larger number + if you don't want to see `buffer` source items while `nvim-lsp` source is available: +>lua cmp.setup { sources = { { name = 'nvim_lsp', group_index = 1 }, @@ -508,8 +585,8 @@ sources[n].group_index~ } } < - You can specify this via the built-in configuration helper like this. -> + You can also achieve this by using the built-in configuration helper like this: +>lua cmp.setup { sources = cmp.config.sources({ { name = 'nvim_lsp' }, @@ -518,62 +595,201 @@ sources[n].group_index~ }) } < + + *cmp-config.sources[n].entry_filter* +sources[n].entry_filter~ + `function` + A source-specific entry filter, with the following function signature: +> + function(entry: cmp.Entry, ctx: cmp.Context): boolean +< + + Returning `true` will keep the entry, while returning `false` will remove it. + + This can be used to hide certain entries from a given source. For instance, you + could hide all entries with kind `Text` from the `nvim_lsp` filter using the + following source definition: +>lua + { + name = 'nvim_lsp', + entry_filter = function(entry, ctx) + return require('cmp.types').lsp.CompletionItemKind[entry:get_kind()] ~= 'Text' + end + } +< + Using the `ctx` parameter, you can further customize the behaviour of the + source. + *cmp-config.view* view~ `{ entries: cmp.EntriesConfig|string }` - Specify the view class to customize appearance. - Currently, the possible configurations are: + The view class used to customize nvim-cmp's appearance. + Currently available configuration options are: + + *cmp-config.window.{completion,documentation}.border* +window.{completion,documentation}.border~ + `string | string[] | nil` + Border characters used for the completion popup menu when |experimental.native_menu| is disabled. + See |nvim_open_win|. + + *cmp-config.window.{completion,documentation}.winhighlight* +window.{completion,documentation}.winhighlight~ + `string | cmp.WinhighlightConfig` + Specify the window's winhighlight option. + See |nvim_open_win|. + + *cmp-config.window.{completion,documentation}.zindex* +window.{completion,documentation}.zindex~ + `number` + The completion window's zindex. + See |nvim_open_win|. + + *cmp-config.window.{completion,documentation}.scrolloff* +window.completion.scrolloff~ + `number` + Specify the window's scrolloff option. + See |'scrolloff'|. + + *cmp-config.window.completion.col_offset* +window.completion.col_offset~ + `number` + Offsets the completion window relative to the cursor. + + *cmp-config.window.completion.side_padding* +window.completion.side_padding~ + `number` + The ammount of padding to add on the completion window's sides + + *cmp-config.window.completion.scrollbar* +window.completion.scrollbar~ + `boolean` + Whether the scrollbar should be enabled if there are more items that fit + + *cmp-config.window.documentation.max_width* +window.documentation.max_width~ + `number` + The documentation window's max width. + + *cmp-config.window.documentation.max_height* +window.documentation.max_height~ + `number` + The documentation window's max height. *cmp-config.experimental.ghost_text* experimental.ghost_text~ `boolean | { hl_group = string }` - The boolean value to enable or disable the ghost_text feature. + Whether to enable the ghost_text feature. +============================================================================== +Config Helper *cmp-config-helper* +You can use the following configuration helpers: +cmp.config.compare~ + + TBD + +cmp.config.context~ + + The `cmp.config.context` can be used for context-aware completion toggling. +>lua + cmp.setup { + enabled = function() + -- disable completion if the cursor is `Comment` syntax group. + return not cmp.config.context.in_syntax_group('Comment') + end + } +< + *cmp.config.context.in_syntax_group* (group) + You can specify the vim's built-in syntax group. + If you use tree-sitter, you should use `cmp.config.context.in_treesitter_capture` instead. + + *cmp.config.context.in_treesitter_capture* (capture) + You can specify the treesitter capture name. + If you don't use the `nvim-treesitter` plugin, this helper will not work correctly. + +cmp.config.mapping~ + + See |cmp-mapping|. + +cmp.config.sources~ + + *cmp.config.sources* (...sources) + You can specify multiple source arrays. The sources are grouped in the + order you specify, and the groups are displayed as a fallback, like chain + completion. +>lua + cmp.setup { + sources = cmp.config.sources({ + { name = 'nvim_lsp' }, + }, { + { name = 'buffer' }, + }) + } +< +cmp.config.window~ + + *cmp.config.window.bordered* (option) + Make the completion window `bordered`. + The option is described in `cmp.ConfigSchema`. +>lua + cmp.setup { + window = { + completion = cmp.config.window.bordered(), + documentation = cmp.config.window.bordered(), + } + } +< ============================================================================== Develop *cmp-develop* -Create custom source~ +Creating a custom source~ NOTE: - 1. The `complete` method is required. Others can be ommited. - 2. The `callback` argument must always be called. + 1. The `complete` method is required. Others can be omitted. + 2. The `callback` function must always be called. 3. You can use only `require('cmp')` in custom source. - 4. If the LSP spec was changed, nvim-cmp will follow it without any announcement. - 5. You should read ./lua/cmp/types and https://microsoft.github.io/language-server-protocol/specifications/specification-current - 6. Please add the `nvim-cmp` topic for github repo. + 4. If the LSP spec was changed, nvim-cmp may implement it without any announcement (potentially introducing breaking changes). + 5. You should read ./lua/cmp/types and https://microsoft.github.io/language-server-protocol/specifications/specification-current. + 6. Please add your source to the list of sources in the Wiki (https://github.com/hrsh7th/nvim-cmp/wiki/List-of-sources) + and if you publish it on GitHub, add the `nvim-cmp` topic so users can find it more easily. -You can create custom source like the following example. - -> +Here is an example on how to create a custom source: +>lua local source = {} - ---Return this source is available in current context or not. (Optional) + ---Return whether this source is available in the current context or not (optional). ---@return boolean function source:is_available() return true end - ---Return the debug name of this source. (Optional) + ---Return the debug name of this source (optional). ---@return string function source:get_debug_name() return 'debug name' end - ---Return keyword pattern for triggering completion. (Optional) - ---If this is ommited, nvim-cmp will use default keyword pattern. See |cmp-config.completion.keyword_pattern| + ---Return LSP's PositionEncodingKind. + ---@NOTE: If this method is ommited, the default value will be `utf-16`. + ---@return lsp.PositionEncodingKind + function source:get_position_encoding_kind() + return 'utf-16' + end + + ---Return the keyword pattern for triggering completion (optional). + ---If this is ommited, nvim-cmp will use a default keyword pattern. See |cmp-config.completion.keyword_pattern|. ---@return string function source:get_keyword_pattern() return [[\k\+]] end - ---Return trigger characters for triggering completion. (Optional) + ---Return trigger characters for triggering completion (optional). function source:get_trigger_characters() return { '.' } end - ---Invoke completion. (Required) + ---Invoke completion (required). ---@param params cmp.SourceCompletionApiParams ---@param callback fun(response: lsp.CompletionResponse|nil) function source:complete(params, callback) @@ -593,47 +809,70 @@ You can create custom source like the following example. }) end - ---Resolve completion item. (Optional) + ---Resolve completion item (optional). This is called right before the completion is about to be displayed. + ---Useful for setting the text shown in the documentation window (`completion_item.documentation`). ---@param completion_item lsp.CompletionItem ---@param callback fun(completion_item: lsp.CompletionItem|nil) function source:resolve(completion_item, callback) callback(completion_item) end - ---Execute command after item was accepted. + ---Executed after the item was selected. ---@param completion_item lsp.CompletionItem ---@param callback fun(completion_item: lsp.CompletionItem|nil) function source:execute(completion_item, callback) callback(completion_item) end - ---Register custom source to nvim-cmp. - require('cmp').register_source('month', source.new()) + ---Register your source to nvim-cmp. + require('cmp').register_source('month', source) < - - ============================================================================== FAQ *cmp-faq* Why does cmp automatically select a particular item? ~ How to disable the preselect feature? ~ - The nvim-cmp respects LSP(Language Server Protocol) specification. - The LSP spec defines the `preselect` feature for completion. + Nvim-cmp respects the LSP (Language Server Protocol) specification. + The LSP spec defines the `preselect` feature for completion. - You can disable the `preselect` feature like the following. -> + You can disable the `preselect` feature like this: +>lua cmp.setup { preselect = cmp.PreselectMode.None } < +How to disable only specific language-server's completion?~ + + You can disable `completionProvider` in lspconfig configuration. +>lua + lspconfig[%SERVER_NAME%].setup { + on_attach = function(client) + client.server_capabilities.completionProvider = false + end + } +< + + +How to disable commitCharacters?~ + + You can disable the commitCharacters feature (which is defined in LSP spec): +>lua + cmp.setup { + confirmation = { + get_commit_characters = function(commit_characters) + return {} + end + } + } +< How to disable auto-completion?~ -How to use nvim-cmp as like omnifunc?~ +How to use nvim-cmp as omnifunc?~ - You can disable auto-completion like this. -> + You can disable auto-completion like this: +>lua cmp.setup { ... completion = { @@ -642,16 +881,16 @@ How to use nvim-cmp as like omnifunc?~ ... } < - And you can invoke completion manually. -> + Then you will need to invoke completion manually. +>vim inoremap lua require('cmp').complete() < -How to disable nvim-cmp on the specific buffer?~ -How to setup on the specific buffer?~ +How to disable nvim-cmp for a specific buffer?~ +How to setup nvim-cmp for a specific buffer?~ - You can setup buffer specific configuration like this. -> + You can setup buffer-specific configuration like this: +>lua cmp.setup.filetype({ 'markdown', 'help' }, { sources = { { name = 'path' }, @@ -660,18 +899,35 @@ How to setup on the specific buffer?~ }) < +How to disable the documentation window?~ + + Simply use the following config: +>lua + cmp.setup.filetype({ 'markdown', 'help' }, { + window = { + documentation = cmp.config.disable + } + }) +< + +I'm using clangd. The menu items are mis-indented.~ + + It's caused by clangd. You can specify `--header-insertion-decorators` for + clangd's command-line arguments. See #999. + + How to integrate with copilot.vim?~ - The copilot.vim and nvim-cmp both have a `key-mapping fallback` mechanism. - Therefore, You should manage those plugins by yourself. + Copilot.vim and nvim-cmp both have a `key-mapping fallback` mechanism. + Therefore, you should manage those plugins by yourself. - Fortunately, the copilot.vim has the feature that disables the fallback mechanism. -> + Fortunately, the copilot.vim has a feature that disables the fallback mechanism. +>vim let g:copilot_no_tab_map = v:true imap (vimrc:copilot-dummy-map) copilot#Accept("\") < - You can manage copilot.vim's accept feature with nvim-cmp' key-mapping configuration. -> + You can manage copilot.vim's accept feature inside nvim-cmp's key-mapping function: +>lua cmp.setup { mapping = { [''] = cmp.mapping(function(fallback) @@ -679,18 +935,20 @@ How to integrate with copilot.vim?~ end) }, experimental = { - ghost_text = false -- this feature conflict to the copilot.vim's preview. + ghost_text = false -- this feature conflict with copilot.vim's preview. } } < +nvim-cmp does not work as expected.~ + There are some known issues. Please check the following. -How to customize menu appearance?~ - - You can see the nvim-cmp wiki (https://github.com/hrsh7th/nvim-cmp/wiki). + - nvim-cmp does not work with `set paste` option. + - Command line mode key mapping is unified regardless of `:`, `/`, `?`. Therefore, it is impossible to apply the mapping only to `:`. +How to customize the menu appearance?~ + Have a look at the wiki (https://github.com/hrsh7th/nvim-cmp/wiki). ============================================================================== - vim:tw=78:ts=4:et:ft=help:norl: - + vim:tw=78:ts=2:et:ft=help:norl: diff --git a/bundle/nvim-cmp/lua/cmp/config.lua b/bundle/nvim-cmp/lua/cmp/config.lua index 1772184b0..efb99c4e9 100644 --- a/bundle/nvim-cmp/lua/cmp/config.lua +++ b/bundle/nvim-cmp/lua/cmp/config.lua @@ -14,7 +14,7 @@ config.cache = cache.new() ---@type cmp.ConfigSchema config.global = require('cmp.config.default')() ----@type table +---@type table config.buffers = {} ---@type table @@ -29,14 +29,14 @@ config.onetime = {} ---Set configuration for global. ---@param c cmp.ConfigSchema config.set_global = function(c) - config.global = misc.merge(config.normalize(c), config.normalize(config.global)) + config.global = config.normalize(misc.merge(c, config.global)) config.global.revision = config.global.revision or 1 config.global.revision = config.global.revision + 1 end ---Set configuration for buffer ---@param c cmp.ConfigSchema ----@param bufnr number|nil +---@param bufnr integer config.set_buffer = function(c, bufnr) local revision = (config.buffers[bufnr] or {}).revision or 1 config.buffers[bufnr] = c or {} @@ -56,11 +56,13 @@ end ---Set configuration for cmdline ---@param c cmp.ConfigSchema ----@param cmdtype string -config.set_cmdline = function(c, cmdtype) - local revision = (config.cmdline[cmdtype] or {}).revision or 1 - config.cmdline[cmdtype] = c or {} - config.cmdline[cmdtype].revision = revision + 1 +---@param cmdtypes string|string[] +config.set_cmdline = function(c, cmdtypes) + for _, cmdtype in ipairs(type(cmdtypes) == 'table' and cmdtypes or { cmdtypes }) do + local revision = (config.cmdline[cmdtype] or {}).revision or 1 + config.cmdline[cmdtype] = c or {} + config.cmdline[cmdtype].revision = revision + 1 + end end ---Set configuration as oneshot completion. @@ -74,7 +76,9 @@ end ---@return cmp.ConfigSchema config.get = function() local global_config = config.global - if config.onetime.sources then + + -- The config object already has `revision` key. + if #vim.tbl_keys(config.onetime) > 1 then local onetime_config = config.onetime return config.cache:ensure({ 'get', @@ -82,7 +86,10 @@ config.get = function() global_config.revision or 0, onetime_config.revision or 0, }, function() - return misc.merge(config.normalize(onetime_config), config.normalize(global_config)) + local c = {} + c = misc.merge(c, config.normalize(onetime_config)) + c = misc.merge(c, config.normalize(global_config)) + return c end) elseif api.is_cmdline_mode() then local cmdtype = vim.fn.getcmdtype() @@ -94,7 +101,10 @@ config.get = function() cmdtype, cmdline_config.revision or 0, }, function() - return misc.merge(config.normalize(cmdline_config), config.normalize(global_config)) + local c = {} + c = misc.merge(c, config.normalize(cmdline_config)) + c = misc.merge(c, config.normalize(global_config)) + return c end) else local bufnr = vim.api.nvim_get_current_buf() @@ -111,9 +121,9 @@ config.get = function() buffer_config.revision or 0, }, function() local c = {} - c = misc.merge(c, config.normalize(buffer_config)) - c = misc.merge(c, config.normalize(filetype_config)) - c = misc.merge(c, config.normalize(global_config)) + c = misc.merge(config.normalize(c), config.normalize(buffer_config)) + c = misc.merge(config.normalize(c), config.normalize(filetype_config)) + c = misc.merge(config.normalize(c), config.normalize(global_config)) return c end) end @@ -144,9 +154,6 @@ end ---Return the current menu is native or not. config.is_native_menu = function() local c = config.get() - if c.experimental and c.experimental.native_menu then - return true - end if c.view and c.view.entries then return c.view.entries == 'native' or c.view.entries.name == 'native' end @@ -154,12 +161,14 @@ config.is_native_menu = function() end ---Normalize mapping key ----@param c cmp.ConfigSchema +---@param c any ---@return cmp.ConfigSchema config.normalize = function(c) -- make sure c is not 'nil' + ---@type any c = c == nil and {} or c + -- Normalize mapping. if c.mapping then local normalized = {} for k, v in pairs(c.mapping) do @@ -168,6 +177,7 @@ config.normalize = function(c) c.mapping = normalized end + -- Notice experimental.native_menu. if c.experimental and c.experimental.native_menu then vim.api.nvim_echo({ { '[nvim-cmp] ', 'Normal' }, @@ -182,6 +192,21 @@ config.normalize = function(c) c.view.entries = c.view.entries or 'native' end + -- Notice documentation. + if c.documentation ~= nil then + vim.api.nvim_echo({ + { '[nvim-cmp] ', 'Normal' }, + { 'documentation', 'WarningMsg' }, + { ' is deprecated.\n', 'Normal' }, + { '[nvim-cmp] Please use ', 'Normal' }, + { 'window.documentation = cmp.config.window.bordered()', 'WarningMsg' }, + { ' instead.', 'Normal' }, + }, true, {}) + c.window = c.window or {} + c.window.documentation = c.documentation + end + + -- Notice sources.[n].opts if c.sources then for _, s in ipairs(c.sources) do if s.opts and not s.option then diff --git a/bundle/nvim-cmp/lua/cmp/config/compare.lua b/bundle/nvim-cmp/lua/cmp/config/compare.lua index 91db9fc40..de8a12001 100644 --- a/bundle/nvim-cmp/lua/cmp/config/compare.lua +++ b/bundle/nvim-cmp/lua/cmp/config/compare.lua @@ -1,6 +1,5 @@ local types = require('cmp.types') local cache = require('cmp.utils.cache') -local misc = require('cmp.utils.misc') local compare = {} @@ -71,7 +70,7 @@ end -- sortText compare.sort_text = function(entry1, entry2) - if misc.safe(entry1.completion_item.sortText) and misc.safe(entry2.completion_item.sortText) then + if entry1.completion_item.sortText and entry2.completion_item.sortText then local diff = vim.stricmp(entry1.completion_item.sortText, entry2.completion_item.sortText) if diff < 0 then return true @@ -108,7 +107,7 @@ compare.locality = setmetatable({ locality_map = {}, update = function(self) local config = require('cmp').get_config() - if not vim.tbl_contains(config.sorting.comparators, compare.scopes) then + if not vim.tbl_contains(config.sorting.comparators, compare.locality) then return end @@ -132,7 +131,7 @@ compare.locality = setmetatable({ local s, e = regexp:match_str(buffer) if s and e then local w = string.sub(buffer, s + 1, e) - local d = math.abs(i - cursor_row) - (is_above and 0.1 or 0) + local d = math.abs(i - cursor_row) - (is_above and 1 or 0) locality_map[w] = math.min(locality_map[w] or math.huge, d) buffer = string.sub(buffer, e + 1) else @@ -145,7 +144,7 @@ compare.locality = setmetatable({ self.locality_map[w] = math.min(self.locality_map[w] or d, math.abs(i - cursor_row)) end end - end + end, }, { __call = function(self, entry1, entry2) local local1 = self.locality_map[entry1:get_word()] @@ -159,7 +158,7 @@ compare.locality = setmetatable({ end return local1 < local2 end - end + end, }) -- scopes @@ -175,7 +174,6 @@ compare.scopes = setmetatable({ if ok then local win, buf = vim.api.nvim_get_current_win(), vim.api.nvim_get_current_buf() local cursor_row = vim.api.nvim_win_get_cursor(win)[1] - 1 - local ts_utils = require('nvim-treesitter.ts_utils') -- Cursor scope. local cursor_scope = nil @@ -205,7 +203,8 @@ compare.scopes = setmetatable({ for _, definition in pairs(definitions) do if s <= definition.node:start() and definition.node:end_() <= e then if scope:id() == locals.containing_scope(definition.node, buf):id() then - local text = ts_utils.get_node_text(definition.node)[1] + local get_node_text = vim.treesitter.get_node_text or vim.treesitter.query.get_node_text + local text = get_node_text(definition.node, buf) or '' if not self.scopes_map[text] then self.scopes_map[text] = depth end diff --git a/bundle/nvim-cmp/lua/cmp/config/context.lua b/bundle/nvim-cmp/lua/cmp/config/context.lua index 584f38abe..c9a87174e 100644 --- a/bundle/nvim-cmp/lua/cmp/config/context.lua +++ b/bundle/nvim-cmp/lua/cmp/config/context.lua @@ -1,65 +1,60 @@ +local api = require('cmp.utils.api') + local context = {} ---Check if cursor is in syntax group ----@param group string +---@param group string | []string ---@return boolean context.in_syntax_group = function(group) - local lnum, col = vim.fn.line('.'), math.min(vim.fn.col('.'), #vim.fn.getline('.')) - for _, syn_id in ipairs(vim.fn.synstack(lnum, col)) do + local row, col = unpack(vim.api.nvim_win_get_cursor(0)) + if not api.is_insert_mode() then + col = col + 1 + end + + for _, syn_id in ipairs(vim.fn.synstack(row, col)) do syn_id = vim.fn.synIDtrans(syn_id) -- Resolve :highlight links - if vim.fn.synIDattr(syn_id, 'name') == group then + local g = vim.fn.synIDattr(syn_id, 'name') + if type(group) == 'string' and g == group then + return true + elseif type(group) == 'table' and vim.tbl_contains(group, g) then return true end end + return false end ---Check if cursor is in treesitter capture ----@param capture string +---@param capture string | []string ---@return boolean context.in_treesitter_capture = function(capture) - local highlighter = require('vim.treesitter.highlighter') - local ts_utils = require('nvim-treesitter.ts_utils') local buf = vim.api.nvim_get_current_buf() - local row, col = unpack(vim.api.nvim_win_get_cursor(0)) row = row - 1 if vim.api.nvim_get_mode().mode == 'i' then col = col - 1 end - local self = highlighter.active[buf] - if not self then + local get_captures_at_pos = -- See neovim/neovim#20331 + require('vim.treesitter').get_captures_at_pos -- for neovim >= 0.8 or require('vim.treesitter').get_captures_at_position -- for neovim < 0.8 + + local captures_at_cursor = vim.tbl_map(function(x) + return x.capture + end, get_captures_at_pos(buf, row, col)) + + if vim.tbl_isempty(captures_at_cursor) then return false - end - - local node_types = {} - - self.tree:for_each_tree(function(tstree, tree) - if not tstree then - return - end - - local root = tstree:root() - local root_start_row, _, root_end_row, _ = root:range() - if root_start_row > row or root_end_row < row then - return - end - - local query = self:get_query(tree:lang()) - if not query:query() then - return - end - - local iter = query:query():iter_captures(root, self.bufnr, row, row + 1) - for _, node, _ in iter do - if ts_utils.is_in_node_range(node, row, col) then - table.insert(node_types, node:type()) + elseif type(capture) == 'string' and vim.tbl_contains(captures_at_cursor, capture) then + return true + elseif type(capture) == 'table' then + for _, v in ipairs(capture) do + if vim.tbl_contains(captures_at_cursor, v) then + return true end end - end, true) + end - return vim.tbl_contains(node_types, capture) + return false end return context diff --git a/bundle/nvim-cmp/lua/cmp/config/default.lua b/bundle/nvim-cmp/lua/cmp/config/default.lua index d596444d6..02296c916 100644 --- a/bundle/nvim-cmp/lua/cmp/config/default.lua +++ b/bundle/nvim-cmp/lua/cmp/config/default.lua @@ -1,13 +1,12 @@ local compare = require('cmp.config.compare') -local mapping = require('cmp.config.mapping') -local keymap = require('cmp.utils.keymap') local types = require('cmp.types') local WIDE_HEIGHT = 40 ---@return cmp.ConfigSchema return function() - return { + ---@type cmp.ConfigSchema + local config = { enabled = function() local disabled = false disabled = disabled or (vim.api.nvim_buf_get_option(0, 'buftype') == 'prompt') @@ -16,85 +15,35 @@ return function() return not disabled end, - preselect = types.cmp.PreselectMode.Item, - - mapping = { - [''] = mapping({ - i = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Select }), - c = function(fallback) - local cmp = require('cmp') - cmp.close() - vim.schedule(cmp.suspend()) - fallback() - end, - }), - [''] = 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, - }), - [''] = 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(''), 'in', true) - else - vim.api.nvim_feedkeys(keymap.t(''), 'in', true) - end - end - end, - }), - [''] = 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(''), 'in', true) - else - vim.api.nvim_feedkeys(keymap.t(''), 'in', true) - end - end - end, - }), - [''] = mapping(mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Insert }), { 'i', 'c' }), - [''] = mapping(mapping.select_prev_item({ behavior = types.cmp.SelectBehavior.Insert }), { 'i', 'c' }), - [''] = mapping.confirm({ select = false }), - [''] = mapping.abort(), + performance = { + debounce = 60, + throttle = 30, + fetching_timeout = 500, + async_budget = 1, + max_view_entries = 200, }, + preselect = types.cmp.PreselectMode.Item, + + mapping = {}, + snippet = { - expand = function() + expand = function(_) error('snippet engine is not configured.') end, }, completion = { - keyword_length = 1, - keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%(-\w*\)*\)]], autocomplete = { types.cmp.TriggerEvent.TextChanged, }, completeopt = 'menu,menuone,noselect', + keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%(-\w*\)*\)]], + keyword_length = 1, }, formatting = { + expandable_indicator = true, fields = { 'abbr', 'kind', 'menu' }, format = function(_, vim_item) return vim_item @@ -103,6 +52,8 @@ return function() matching = { disallow_fuzzy_matching = false, + disallow_fullfuzzy_matching = false, + disallow_partial_fuzzy_matching = true, disallow_partial_matching = false, disallow_prefix_unmatching = false, }, @@ -117,7 +68,7 @@ return function() compare.recently_used, compare.locality, compare.kind, - compare.sort_text, + -- compare.sort_text, compare.length, compare.order, }, @@ -125,13 +76,6 @@ return function() sources = {}, - documentation = { - border = { '', '', '', ' ', '', '', '', ' ' }, - winhighlight = 'NormalFloat:NormalFloat,FloatBorder:NormalFloat', - maxwidth = math.floor((WIDE_HEIGHT * 2) * (vim.o.columns / (WIDE_HEIGHT * 2 * 16 / 9))), - maxheight = math.floor(WIDE_HEIGHT * (WIDE_HEIGHT / vim.o.lines)), - }, - confirmation = { default_behavior = types.cmp.ConfirmBehavior.Insert, get_commit_characters = function(commit_characters) @@ -146,7 +90,28 @@ return function() }, view = { - entries = { name = 'custom', selection_order = 'top_down' }, + entries = { + name = 'custom', + selection_order = 'top_down', + }, + }, + + window = { + completion = { + border = { '', '', '', '', '', '', '', '' }, + winhighlight = 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None', + scrolloff = 0, + col_offset = 0, + side_padding = 1, + scrollbar = true, + }, + documentation = { + max_height = math.floor(WIDE_HEIGHT * (WIDE_HEIGHT / vim.o.lines)), + max_width = math.floor((WIDE_HEIGHT * 2) * (vim.o.columns / (WIDE_HEIGHT * 2 * 16 / 9))), + border = { '', '', '', ' ', '', '', '', ' ' }, + winhighlight = 'FloatBorder:NormalFloat', + }, }, } + return config end diff --git a/bundle/nvim-cmp/lua/cmp/config/mapping.lua b/bundle/nvim-cmp/lua/cmp/config/mapping.lua index 0c2e81ae6..d5d11a518 100644 --- a/bundle/nvim-cmp/lua/cmp/config/mapping.lua +++ b/bundle/nvim-cmp/lua/cmp/config/mapping.lua @@ -1,5 +1,22 @@ -local mapping -mapping = setmetatable({}, { +local types = require('cmp.types') +local misc = require('cmp.utils.misc') +local keymap = require('cmp.utils.keymap') + +local function merge_keymaps(base, override) + local normalized_base = {} + for k, v in pairs(base) do + normalized_base[keymap.normalize(k)] = v + end + + local normalized_override = {} + for k, v in pairs(override) do + normalized_override[keymap.normalize(k)] = v + end + + return misc.merge(normalized_base, normalized_override) +end + +local mapping = setmetatable({}, { __call = function(_, invoke, modes) if type(invoke) == 'function' then local map = {} @@ -12,8 +29,111 @@ mapping = setmetatable({}, { end, }) +---Mapping preset configuration. +mapping.preset = {} + +---Mapping preset insert-mode configuration. +mapping.preset.insert = function(override) + return merge_keymaps(override or {}, { + [''] = { + i = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Select }), + }, + [''] = { + i = mapping.select_prev_item({ behavior = types.cmp.SelectBehavior.Select }), + }, + [''] = { + i = function() + local cmp = require('cmp') + if cmp.visible() then + cmp.select_next_item({ behavior = types.cmp.SelectBehavior.Insert }) + else + cmp.complete() + end + end, + }, + [''] = { + i = function() + local cmp = require('cmp') + if cmp.visible() then + cmp.select_prev_item({ behavior = types.cmp.SelectBehavior.Insert }) + else + cmp.complete() + end + end, + }, + [''] = { + i = mapping.confirm({ select = false }), + }, + [''] = { + i = mapping.abort(), + }, + }) +end + +---Mapping preset cmdline-mode configuration. +mapping.preset.cmdline = function(override) + return merge_keymaps(override or {}, { + [''] = { + c = function() + local cmp = require('cmp') + if cmp.visible() then + cmp.select_next_item() + else + cmp.complete() + end + end, + }, + [''] = { + c = function() + local cmp = require('cmp') + if cmp.visible() then + cmp.select_next_item() + else + cmp.complete() + end + end, + }, + [''] = { + c = function() + local cmp = require('cmp') + if cmp.visible() then + cmp.select_prev_item() + else + cmp.complete() + end + end, + }, + [''] = { + c = function(fallback) + local cmp = require('cmp') + if cmp.visible() then + cmp.select_next_item() + else + fallback() + end + end, + }, + [''] = { + c = function(fallback) + local cmp = require('cmp') + if cmp.visible() then + cmp.select_prev_item() + else + fallback() + end + end, + }, + [''] = { + c = mapping.abort(), + }, + [''] = { + c = mapping.confirm({ select = false }), + }, + }) +end + ---Invoke completion ----@param option cmp.CompleteParams +---@param option? cmp.CompleteParams mapping.complete = function(option) return function(fallback) if not require('cmp').complete(option) then diff --git a/bundle/nvim-cmp/lua/cmp/config/window.lua b/bundle/nvim-cmp/lua/cmp/config/window.lua new file mode 100644 index 000000000..770ee80df --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/config/window.lua @@ -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 diff --git a/bundle/nvim-cmp/lua/cmp/context.lua b/bundle/nvim-cmp/lua/cmp/context.lua index 6188259eb..0411a5440 100644 --- a/bundle/nvim-cmp/lua/cmp/context.lua +++ b/bundle/nvim-cmp/lua/cmp/context.lua @@ -10,12 +10,13 @@ local api = require('cmp.utils.api') ---@field public prev_context cmp.Context ---@field public option cmp.ContextOption ---@field public filetype string ----@field public time number ----@field public bufnr number +---@field public time integer +---@field public bufnr integer ---@field public cursor vim.Position|lsp.Position ---@field public cursor_line string ---@field public cursor_after_line string ---@field public cursor_before_line string +---@field public aborted boolean local context = {} ---Create new empty context @@ -31,8 +32,8 @@ context.empty = function() end ---Create new context ----@param prev_context cmp.Context ----@param option cmp.ContextOption +---@param prev_context? cmp.Context +---@param option? cmp.ContextOption ---@return cmp.Context context.new = function(prev_context, option) option = option or {} @@ -55,9 +56,14 @@ context.new = function(prev_context, option) self.cursor.character = misc.to_utfindex(self.cursor_line, self.cursor.col) self.cursor_before_line = string.sub(self.cursor_line, 1, self.cursor.col - 1) self.cursor_after_line = string.sub(self.cursor_line, self.cursor.col) + self.aborted = false return self end +context.abort = function(self) + self.aborted = true +end + ---Return context creation reason. ---@return cmp.ContextReason context.get_reason = function(self) @@ -65,7 +71,7 @@ context.get_reason = function(self) end ---Get keyword pattern offset ----@return number|nil +---@return integer context.get_offset = function(self, keyword_pattern) return self.cache:ensure({ 'get_offset', keyword_pattern, self.cursor_before_line }, function() return pattern.offset(keyword_pattern .. '\\m$', self.cursor_before_line) or self.cursor.col diff --git a/bundle/nvim-cmp/lua/cmp/core.lua b/bundle/nvim-cmp/lua/cmp/core.lua index 169319dd9..2775cf26a 100644 --- a/bundle/nvim-cmp/lua/cmp/core.lua +++ b/bundle/nvim-cmp/lua/cmp/core.lua @@ -14,10 +14,6 @@ local types = require('cmp.types') local api = require('cmp.utils.api') local event = require('cmp.utils.event') -local SOURCE_TIMEOUT = 500 -local DEBOUNCE_TIME = 80 -local THROTTLE_TIME = 40 - ---@class cmp.Core ---@field public suspending boolean ---@field public view cmp.View @@ -36,9 +32,11 @@ core.new = function() self.view.event:on('keymap', function(...) self:on_keymap(...) end) - self.view.event:on('complete_done', function(evt) - self.event:emit('complete_done', evt) - end) + for _, event_name in ipairs({ 'complete_done', 'menu_opened', 'menu_closed' }) do + self.view.event:on(event_name, function(evt) + self.event:emit(event_name, evt) + end) + end return self end @@ -49,17 +47,19 @@ core.register_source = function(self, s) end ---Unregister source ----@param source_id string +---@param source_id integer core.unregister_source = function(self, source_id) self.sources[source_id] = nil end ---Get new context ----@param option cmp.ContextOption +---@param option? cmp.ContextOption ---@return cmp.Context core.get_context = function(self, option) + self.context:abort() local prev = self.context:clone() prev.prev_context = nil + prev.cache = nil local ctx = context.new(prev, option) self:set_context(ctx) return self.context @@ -74,13 +74,14 @@ end ---Suspend completion core.suspend = function(self) self.suspending = true - return function() + -- It's needed to avoid conflicting with autocmd debouncing. + return vim.schedule_wrap(function() self.suspending = false - end + end) end ---Get sources that sorted by priority ----@param filter cmp.SourceStatus[]|fun(s: cmp.Source): boolean +---@param filter? cmp.SourceStatus[]|fun(s: cmp.Source): boolean ---@return cmp.Source[] core.get_sources = function(self, filter) local f = function(s) @@ -168,7 +169,7 @@ core.on_change = function(self, trigger_event) if vim.tbl_contains(config.get().completion.autocomplete or {}, trigger_event) then self:complete(ctx) else - self.filter.timeout = self.view:visible() and THROTTLE_TIME or 0 + self.filter.timeout = self.view:visible() and config.get().performance.throttle or 0 self:filter() end else @@ -221,7 +222,7 @@ end ---Complete common string for current completed entries. core.complete_common_string = function(self) - if not self.view:visible() or self.view:get_active_entry() then + if not self.view:visible() or self.view:get_selected_entry() then return false end @@ -240,7 +241,7 @@ core.complete_common_string = function(self) config.set_onetime({}) local cursor = api.get_cursor() - local offset = self.view:get_offset() + local offset = self.view:get_offset() or cursor[2] local common_string for _, e in ipairs(self.view:get_entries()) do local vim_item = e:get_vim_item(offset) @@ -250,8 +251,10 @@ core.complete_common_string = function(self) common_string = str.get_common_string(common_string, vim_item.word) end end - if common_string and #common_string > (1 + cursor[2] - offset) then - feedkeys.call(keymap.backspace(string.sub(api.get_current_line(), offset, cursor[2])) .. common_string, 'n') + local cursor_before_line = api.get_cursor_before_line() + local pretext = cursor_before_line:sub(offset) + if common_string and #common_string > #pretext then + feedkeys.call(keymap.backspace(pretext) .. common_string, 'n') return true end return false @@ -276,17 +279,9 @@ core.complete = function(self, ctx) if s_.incomplete and new:changed(s_.context) then s_:complete(new, callback) else - for _, s__ in ipairs(self:get_sources({ source.SourceStatus.FETCHING })) do - if s_ == s__ then - break - end - if not s__.incomplete and SOURCE_TIMEOUT > s__:get_fetching_time() then - return - end - end if not self.view:get_active_entry() then self.filter.stop() - self.filter.timeout = self.view:visible() and DEBOUNCE_TIME or 0 + self.filter.timeout = config.get().performance.debounce self:filter() end end @@ -296,14 +291,14 @@ core.complete = function(self, ctx) end if not self.view:get_active_entry() then - self.filter.timeout = self.view:visible() and THROTTLE_TIME or 0 + self.filter.timeout = self.view:visible() and config.get().performance.throttle or 1 self:filter() end end ---Update completion menu -core.filter = async.throttle(function(self) - self.filter.timeout = self.view:visible() and THROTTLE_TIME or 0 +local async_filter = async.wrap(function(self) + self.filter.timeout = config.get().performance.throttle -- Check invalid condition. local ignore = false @@ -315,11 +310,13 @@ core.filter = async.throttle(function(self) -- Check fetching sources. local sources = {} for _, s in ipairs(self:get_sources({ source.SourceStatus.FETCHING, source.SourceStatus.COMPLETED })) do - if not s.incomplete and SOURCE_TIMEOUT > s:get_fetching_time() then - -- Reserve filter call for timeout. - self.filter.timeout = SOURCE_TIMEOUT - s:get_fetching_time() + -- Reserve filter call for timeout. + if not s.incomplete and config.get().performance.fetching_timeout > s:get_fetching_time() then + self.filter.timeout = config.get().performance.fetching_timeout - s:get_fetching_time() self:filter() - break + if #sources == 0 then + return + end end table.insert(sources, s) end @@ -327,20 +324,17 @@ core.filter = async.throttle(function(self) local ctx = self:get_context() -- Display completion results. - self.view:open(ctx, sources) + local did_open = self.view:open(ctx, sources) + local fetching = #self:get_sources(function(s) + return s.status == source.SourceStatus.FETCHING + end) -- Check onetime config. - if #self:get_sources(function(s) - if s.status == source.SourceStatus.FETCHING then - return true - elseif #s:get_entries(ctx) > 0 then - return true - end - return false - end) == 0 then + if not did_open and fetching == 0 then config.set_onetime({}) end -end, THROTTLE_TIME) +end) +core.filter = async.throttle(async_filter, config.get().performance.throttle) ---Confirm completion. ---@param e cmp.Entry @@ -348,7 +342,10 @@ end, THROTTLE_TIME) ---@param callback function core.confirm = function(self, e, option, callback) if not (e and not e.confirmed) then - return callback() + if callback then + callback() + end + return end e.confirmed = true @@ -361,33 +358,39 @@ core.confirm = function(self, e, option, callback) feedkeys.call(keymap.indentkeys(), 'n') feedkeys.call('', 'n', function() + -- Emulate `` behavior to save `.` register. local ctx = context.new() local keys = {} - table.insert(keys, keymap.backspace(ctx.cursor.character - misc.to_utfindex(ctx.cursor_line, e:get_offset()))) + table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e:get_offset()))) table.insert(keys, e:get_word()) table.insert(keys, keymap.undobreak()) - feedkeys.call(table.concat(keys, ''), 'int') + feedkeys.call(table.concat(keys, ''), 'in') end) feedkeys.call('', 'n', function() + -- Restore the line at the time of request. local ctx = context.new() if api.is_cmdline_mode() then local keys = {} - table.insert(keys, keymap.backspace(ctx.cursor.character - misc.to_utfindex(ctx.cursor_line, e:get_offset()))) + table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e:get_offset()))) table.insert(keys, string.sub(e.context.cursor_before_line, e:get_offset())) feedkeys.call(table.concat(keys, ''), 'in') else - vim.api.nvim_buf_set_text(0, ctx.cursor.row - 1, e:get_offset() - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, { - string.sub(e.context.cursor_before_line, e:get_offset()), + vim.cmd([[silent! undojoin]]) + -- This logic must be used nvim_buf_set_text. + -- If not used, the snippet engine's placeholder wil be broken. + vim.api.nvim_buf_set_text(0, e.context.cursor.row - 1, e:get_offset() - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, { + e.context.cursor_before_line:sub(e:get_offset()), }) vim.api.nvim_win_set_cursor(0, { e.context.cursor.row, e.context.cursor.col - 1 }) end end) feedkeys.call('', 'n', function() + -- Apply additionalTextEdits. local ctx = context.new() - if #(misc.safe(e:get_completion_item().additionalTextEdits) or {}) == 0 then + if #(e:get_completion_item().additionalTextEdits or {}) == 0 then e:resolve(function() local new = context.new() - local text_edits = misc.safe(e:get_completion_item().additionalTextEdits) or {} + local text_edits = e:get_completion_item().additionalTextEdits or {} if #text_edits == 0 then return end @@ -407,18 +410,20 @@ core.confirm = function(self, e, option, callback) if has_cursor_line_text_edit then return end - vim.lsp.util.apply_text_edits(text_edits, ctx.bufnr, 'utf-16') + vim.cmd([[silent! undojoin]]) + vim.lsp.util.apply_text_edits(text_edits, ctx.bufnr, e.source:get_position_encoding_kind()) end) else - vim.lsp.util.apply_text_edits(e:get_completion_item().additionalTextEdits, ctx.bufnr, 'utf-16') + vim.cmd([[silent! undojoin]]) + vim.lsp.util.apply_text_edits(e:get_completion_item().additionalTextEdits, ctx.bufnr, e.source:get_position_encoding_kind()) end end) feedkeys.call('', 'n', function() local ctx = context.new() local completion_item = misc.copy(e:get_completion_item()) - if not misc.safe(completion_item.textEdit) then + if not completion_item.textEdit then completion_item.textEdit = {} - completion_item.textEdit.newText = misc.safe(completion_item.insertText) or completion_item.word or completion_item.label + completion_item.textEdit.newText = completion_item.insertText or completion_item.word or completion_item.label end local behavior = option.behavior or config.get().confirmation.default_behavior if behavior == types.cmp.ConfirmBehavior.Replace then @@ -427,30 +432,41 @@ core.confirm = function(self, e, option, callback) completion_item.textEdit.range = e:get_insert_range() end - local diff_before = math.max(0, e.context.cursor.character - completion_item.textEdit.range.start.character) - local diff_after = math.max(0, completion_item.textEdit.range['end'].character - e.context.cursor.character) + local diff_before = math.max(0, e.context.cursor.col - (completion_item.textEdit.range.start.character + 1)) + local diff_after = math.max(0, (completion_item.textEdit.range['end'].character + 1) - e.context.cursor.col) local new_text = completion_item.textEdit.newText - + completion_item.textEdit.range.start.line = ctx.cursor.line + completion_item.textEdit.range.start.character = (ctx.cursor.col - 1) - diff_before + completion_item.textEdit.range['end'].line = ctx.cursor.line + completion_item.textEdit.range['end'].character = (ctx.cursor.col - 1) + diff_after if api.is_insert_mode() then + if false then + --To use complex expansion debug. + vim.print({ -- luacheck: ignore + item = e:get_completion_item(), + diff_before = diff_before, + diff_after = diff_after, + new_text = new_text, + text_edit_new_text = completion_item.textEdit.newText, + range_start = completion_item.textEdit.range.start.character, + range_end = completion_item.textEdit.range['end'].character, + original_range_start = e:get_completion_item().textEdit.range.start.character, + original_range_end = e:get_completion_item().textEdit.range['end'].character, + cursor_line = ctx.cursor_line, + cursor_col0 = ctx.cursor.col - 1, + }) + end local is_snippet = completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet - completion_item.textEdit.range.start.line = ctx.cursor.line - completion_item.textEdit.range.start.character = ctx.cursor.character - diff_before - completion_item.textEdit.range['end'].line = ctx.cursor.line - completion_item.textEdit.range['end'].character = ctx.cursor.character + diff_after if is_snippet then completion_item.textEdit.newText = '' end - vim.lsp.util.apply_text_edits({ completion_item.textEdit }, ctx.bufnr, 'utf-16') + vim.lsp.util.apply_text_edits({ completion_item.textEdit }, ctx.bufnr, 'utf-8') + local texts = vim.split(completion_item.textEdit.newText, '\n') - local position = completion_item.textEdit.range.start - position.line = position.line + (#texts - 1) - if #texts == 1 then - position.character = position.character + misc.to_utfindex(texts[1]) - else - position.character = misc.to_utfindex(texts[#texts]) - end - local pos = types.lsp.Position.to_vim(0, position) - vim.api.nvim_win_set_cursor(0, { pos.row, pos.col - 1 }) + vim.api.nvim_win_set_cursor(0, { + completion_item.textEdit.range.start.line + #texts, + (#texts == 1 and (completion_item.textEdit.range.start.character + #texts[1]) or #texts[#texts]), + }) if is_snippet then config.get().snippet.expand({ body = new_text, @@ -459,8 +475,8 @@ core.confirm = function(self, e, option, callback) end else local keys = {} - table.insert(keys, string.rep(keymap.t(''), diff_before)) - table.insert(keys, string.rep(keymap.t(''), diff_after)) + table.insert(keys, keymap.backspace(ctx.cursor_line:sub(completion_item.textEdit.range.start.character + 1, ctx.cursor.col - 1))) + table.insert(keys, keymap.delete(ctx.cursor_line:sub(ctx.cursor.col, completion_item.textEdit.range['end'].character))) table.insert(keys, new_text) feedkeys.call(table.concat(keys, ''), 'in') end diff --git a/bundle/nvim-cmp/lua/cmp/core_spec.lua b/bundle/nvim-cmp/lua/cmp/core_spec.lua index c37090e2b..eb0c9326d 100644 --- a/bundle/nvim-cmp/lua/cmp/core_spec.lua +++ b/bundle/nvim-cmp/lua/cmp/core_spec.lua @@ -8,9 +8,19 @@ local api = require('cmp.utils.api') describe('cmp.core', function() describe('confirm', function() - local confirm = function(request, filter, completion_item) + ---@param request string + ---@param filter string + ---@param completion_item lsp.CompletionItem + ---@param option? { position_encoding_kind: lsp.PositionEncodingKind } + ---@return table + local confirm = function(request, filter, completion_item, option) + option = option or {} + local c = core.new() local s = source.new('spec', { + get_position_encoding_kind = function() + return option.position_encoding_kind or types.lsp.PositionEncodingKind.UTF16 + end, complete = function(_, _, callback) callback({ completion_item }) end, @@ -23,7 +33,7 @@ describe('cmp.core', function() end) end) feedkeys.call(filter, 'n', function() - c:confirm(c.sources[s.id].entries[1], {}) + c:confirm(c.sources[s.id].entries[1], {}, function() end) end) local state = {} feedkeys.call('', 'x', function() @@ -80,6 +90,29 @@ describe('cmp.core', function() assert.are.same(state.cursor, { 3, 3 }) end) + it('#1552', function() + local state = confirm(keymap.t('ios.'), '', { + filterText = 'IsPermission', + insertTextFormat = 2, + label = 'IsPermission', + textEdit = { + newText = 'IsPermission($0)', + range = { + ['end'] = { + character = 3, + line = 0, + }, + start = { + character = 3, + line = 0, + }, + }, + }, + }) + assert.are.same(state.buffer, { 'os.IsPermission()' }) + assert.are.same(state.cursor, { 1, 16 }) + end) + it('insertText & snippet', function() local state = confirm('iA', 'IU', { label = 'AIUEO', @@ -111,6 +144,46 @@ describe('cmp.core', function() assert.are.same(state.buffer, { '***foo', 'bar', 'baz***' }) assert.are.same(state.cursor, { 2, 2 }) end) + + local char = '🗿' + for _, case in ipairs({ + { + encoding = types.lsp.PositionEncodingKind.UTF8, + char_size = #char, + }, + { + encoding = types.lsp.PositionEncodingKind.UTF16, + char_size = select(2, vim.str_utfindex(char)), + }, + { + encoding = types.lsp.PositionEncodingKind.UTF32, + char_size = select(1, vim.str_utfindex(char)), + }, + }) do + it('textEdit & multibyte: ' .. case.encoding, function() + local state = confirm(keymap.t('i%s:%s%s:%s'):format(char, char, char, char), char, { + label = char .. char .. char, + textEdit = { + range = { + start = { + line = 0, + character = case.char_size + #':', + }, + ['end'] = { + line = 0, + character = case.char_size + #':' + case.char_size + case.char_size, + }, + }, + newText = char .. char .. char .. char .. char, + }, + }, { + position_encoding_kind = case.encoding, + }) + vim.print({ state = state, case = case }) + assert.are.same(state.buffer, { ('%s:%s%s%s%s%s:%s'):format(char, char, char, char, char, char, char) }) + assert.are.same(state.cursor, { 1, #('%s:%s%s%s%s%s'):format(char, char, char, char, char, char) }) + end) + end end) describe('cmdline-mode', function() diff --git a/bundle/nvim-cmp/lua/cmp/entry.lua b/bundle/nvim-cmp/lua/cmp/entry.lua index 83c53a209..50b6535ad 100644 --- a/bundle/nvim-cmp/lua/cmp/entry.lua +++ b/bundle/nvim-cmp/lua/cmp/entry.lua @@ -7,15 +7,15 @@ local types = require('cmp.types') local matcher = require('cmp.matcher') ---@class cmp.Entry ----@field public id number +---@field public id integer ---@field public cache cmp.Cache ---@field public match_cache cmp.Cache ----@field public score number +---@field public score integer ---@field public exact boolean ---@field public matches table ---@field public context cmp.Context ---@field public source cmp.Source ----@field public source_offset number +---@field public source_offset integer ---@field public source_insert_range lsp.Range ---@field public source_replace_range lsp.Range ---@field public completion_item lsp.CompletionItem @@ -29,8 +29,9 @@ local entry = {} ---@param ctx cmp.Context ---@param source cmp.Source ---@param completion_item lsp.CompletionItem +---@param item_defaults? lsp.internal.CompletionItemDefaults ---@return cmp.Entry -entry.new = function(ctx, source, completion_item) +entry.new = function(ctx, source, completion_item, item_defaults) local self = setmetatable({}, { __index = entry }) self.id = misc.id('entry.new') self.cache = cache.new() @@ -43,7 +44,7 @@ entry.new = function(ctx, source, completion_item) self.source_offset = source.request_offset self.source_insert_range = source:get_default_insert_range() self.source_replace_range = source:get_default_replace_range() - self.completion_item = completion_item + self.completion_item = self:fill_defaults(completion_item, item_defaults) self.resolved_completion_item = nil self.resolved_callbacks = {} self.resolving = false @@ -52,20 +53,23 @@ entry.new = function(ctx, source, completion_item) end ---Make offset value ----@return number +---@return integer entry.get_offset = function(self) - return self.cache:ensure({ 'get_offset', self.resolved_completion_item and 1 or 0 }, function() + return self.cache:ensure('get_offset', function() local offset = self.source_offset - if misc.safe(self:get_completion_item().textEdit) then - local range = misc.safe(self:get_completion_item().textEdit.insert) or misc.safe(self:get_completion_item().textEdit.range) + if self:get_completion_item().textEdit then + local range = self:get_insert_range() if range then - local c = misc.to_vimindex(self.context.cursor_line, range.start.character) - for idx = c, self.source_offset do - if not char.is_white(string.byte(self.context.cursor_line, idx)) then - offset = idx - break + offset = self.context.cache:ensure('entry:' .. 'get_offset:' .. tostring(range.start.character), function() + local start = math.min(range.start.character + 1, offset) + for idx = start, self.source_offset do + local byte = string.byte(self.context.cursor_line, idx) + if byte == nil or not char.is_white(byte) then + return idx + end end - end + return offset + end) end else -- NOTE @@ -101,14 +105,14 @@ end ---NOTE: This method doesn't clear the cache after completionItem/resolve. ---@return string entry.get_word = function(self) - return self.cache:ensure({ 'get_word' }, function() + return self.cache:ensure('get_word', function() --NOTE: This is nvim-cmp specific implementation. - if misc.safe(self:get_completion_item().word) then + if self:get_completion_item().word then return self:get_completion_item().word end local word - if misc.safe(self:get_completion_item().textEdit) and not misc.empty(self:get_completion_item().textEdit.newText) then + if self:get_completion_item().textEdit and not misc.empty(self:get_completion_item().textEdit.newText) then word = str.trim(self:get_completion_item().textEdit.newText) if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then word = vim.lsp.util.parse_snippet(word) @@ -126,20 +130,24 @@ entry.get_word = function(self) word = str.trim(self:get_completion_item().label) end return str.oneline(word) - end) + end) --[[@as string]] end ---Get overwrite information ----@return number, number +---@return integer[] entry.get_overwrite = function(self) - return self.cache:ensure({ 'get_overwrite', self.resolved_completion_item and 1 or 0 }, function() - if misc.safe(self:get_completion_item().textEdit) then - local r = misc.safe(self:get_completion_item().textEdit.insert) or misc.safe(self:get_completion_item().textEdit.range) - local s = misc.to_vimindex(self.context.cursor_line, r.start.character) - local e = misc.to_vimindex(self.context.cursor_line, r['end'].character) - local before = self.context.cursor.col - s - local after = e - self.context.cursor.col - return { before, after } + return self.cache:ensure('get_overwrite', function() + if self:get_completion_item().textEdit then + local range = self:get_insert_range() + if range then + return self.context.cache:ensure('entry:' .. 'get_overwrite:' .. tostring(range.start.character) .. ':' .. tostring(range['end'].character), function() + local vim_start = range.start.character + 1 + local vim_end = range['end'].character + 1 + local before = self.context.cursor.col - vim_start + local after = vim_end - self.context.cursor.col + return { before, after } + end) + end end return { 0, 0 } end) @@ -148,9 +156,9 @@ end ---Create filter text ---@return string entry.get_filter_text = function(self) - return self.cache:ensure({ 'get_filter_text', self.resolved_completion_item and 1 or 0 }, function() + return self.cache:ensure('get_filter_text', function() local word - if misc.safe(self:get_completion_item().filterText) then + if self:get_completion_item().filterText then word = self:get_completion_item().filterText else word = str.trim(self:get_completion_item().label) @@ -162,14 +170,14 @@ end ---Get LSP's insert text ---@return string entry.get_insert_text = function(self) - return self.cache:ensure({ 'get_insert_text', self.resolved_completion_item and 1 or 0 }, function() + return self.cache:ensure('get_insert_text', function() local word - if misc.safe(self:get_completion_item().textEdit) then + if self:get_completion_item().textEdit then word = str.trim(self:get_completion_item().textEdit.newText) if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}') end - elseif misc.safe(self:get_completion_item().insertText) then + elseif self:get_completion_item().insertText then word = str.trim(self:get_completion_item().insertText) if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}') @@ -188,12 +196,12 @@ entry.is_deprecated = function(self) end ---Return view information. ----@param suggest_offset number ----@param entries_buf number The buffer this entry will be rendered into. ----@return { abbr: { text: string, bytes: number, width: number, hl_group: string }, kind: { text: string, bytes: number, width: number, hl_group: string }, menu: { text: string, bytes: number, width: number, hl_group: string } } +---@param suggest_offset integer +---@param entries_buf integer The buffer this entry will be rendered into. +---@return { abbr: { text: string, bytes: integer, width: integer, hl_group: string }, kind: { text: string, bytes: integer, width: integer, hl_group: string }, menu: { text: string, bytes: integer, width: integer, hl_group: string } } entry.get_view = function(self, suggest_offset, entries_buf) local item = self:get_vim_item(suggest_offset) - return self.cache:ensure({ 'get_view', self.resolved_completion_item and 1 or 0, entries_buf }, function() + return self.cache:ensure('get_view:' .. tostring(entries_buf), function() local view = {} -- The result of vim.fn.strdisplaywidth depends on which buffer it was -- called in because it reads the values of the option 'tabstop' when @@ -221,24 +229,25 @@ entry.get_view = function(self, suggest_offset, entries_buf) end ---Make vim.CompletedItem ----@param suggest_offset number +---@param suggest_offset integer ---@return vim.CompletedItem entry.get_vim_item = function(self, suggest_offset) - return self.cache:ensure({ 'get_vim_item', suggest_offset, self.resolved_completion_item and 1 or 0 }, function() + return self.cache:ensure('get_vim_item:' .. tostring(suggest_offset), function() local completion_item = self:get_completion_item() local word = self:get_word() local abbr = str.oneline(completion_item.label) -- ~ indicator - local is_snippet = false - if #(misc.safe(completion_item.additionalTextEdits) or {}) > 0 then - is_snippet = true + local is_expandable = false + local expandable_indicator = config.get().formatting.expandable_indicator + if #(completion_item.additionalTextEdits or {}) > 0 then + is_expandable = true elseif completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then - is_snippet = self:get_insert_text() ~= word + is_expandable = self:get_insert_text() ~= word elseif completion_item.kind == types.lsp.CompletionItemKind.Snippet then - is_snippet = true + is_expandable = true end - if is_snippet then + if expandable_indicator and is_expandable then abbr = abbr .. '~' end @@ -249,19 +258,19 @@ entry.get_vim_item = function(self, suggest_offset) -- labelDetails. local menu = nil - if misc.safe(completion_item.labelDetails) then + if completion_item.labelDetails then menu = '' - if misc.safe(completion_item.labelDetails.detail) then + if completion_item.labelDetails.detail then menu = menu .. completion_item.labelDetails.detail end - if misc.safe(completion_item.labelDetails.description) then + if completion_item.labelDetails.description then menu = menu .. completion_item.labelDetails.description end end -- remove duplicated string. if self:get_offset() ~= self.context.cursor.col then - for i = 1, #word - 1 do + for i = 1, #word do if str.has_prefix(self.context.cursor_after_line, string.sub(word, i, #word)) then word = string.sub(word, 1, i - 1) break @@ -269,10 +278,13 @@ entry.get_vim_item = function(self, suggest_offset) end end + local cmp_opts = self:get_completion_item().cmp or {} + local vim_item = { word = word, abbr = abbr, - kind = types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1], + kind = cmp_opts.kind_text or types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1], + kind_hl_group = cmp_opts.kind_hl_group, menu = menu, dup = self:get_completion_item().dup or 1, } @@ -293,24 +305,25 @@ end ---Get commit characters ---@return string[] entry.get_commit_characters = function(self) - return misc.safe(self:get_completion_item().commitCharacters) or {} + return self:get_completion_item().commitCharacters or {} end ---Return insert range ---@return lsp.Range|nil entry.get_insert_range = function(self) local insert_range - if misc.safe(self:get_completion_item().textEdit) then - if misc.safe(self:get_completion_item().textEdit.insert) then + if self:get_completion_item().textEdit then + if self:get_completion_item().textEdit.insert then insert_range = self:get_completion_item().textEdit.insert else - insert_range = self:get_completion_item().textEdit.range + insert_range = self:get_completion_item().textEdit.range --[[@as lsp.Range]] end + insert_range = self:convert_range_encoding(insert_range) else insert_range = { start = { line = self.context.cursor.row - 1, - character = math.min(misc.to_utfindex(self.context.cursor_line, self:get_offset()), self.source_insert_range.start.character), + character = self:get_offset() - 1, }, ['end'] = self.source_insert_range['end'], } @@ -321,15 +334,22 @@ end ---Return replace range ---@return lsp.Range|nil entry.get_replace_range = function(self) - return self.cache:ensure({ 'get_replace_range', self.resolved_completion_item and 1 or 0 }, function() + return self.cache:ensure('get_replace_range', function() local replace_range - if misc.safe(self:get_completion_item().textEdit) and misc.safe(self:get_completion_item().textEdit.replace) then - replace_range = self:get_completion_item().textEdit.replace - else + if self:get_completion_item().textEdit then + if self:get_completion_item().textEdit.replace then + replace_range = self:get_completion_item().textEdit.replace + else + replace_range = self:get_completion_item().textEdit.range --[[@as lsp.Range]] + end + replace_range = self:convert_range_encoding(replace_range) + end + + if not replace_range or ((self.context.cursor.col - 1) == replace_range['end'].character) then replace_range = { start = { line = self.source_replace_range.start.line, - character = math.min(misc.to_utfindex(self.context.cursor_line, self:get_offset()), self.source_replace_range.start.character), + character = self:get_offset() - 1, }, ['end'] = self.source_replace_range['end'], } @@ -341,17 +361,12 @@ end ---Match line. ---@param input string ---@param matching_config cmp.MatchingConfig ----@return { score: number, matches: table[] } +---@return { score: integer, matches: table[] } entry.match = function(self, input, matching_config) - return self.match_cache:ensure({ - input, - self.resolved_completion_item and 1 or 0, - matching_config.disallow_fuzzy_matching and 1 or 0, - matching_config.disallow_partial_matching and 1 or 0, - matching_config.disallow_prefix_unmatching and 1 or 0, - }, function() + return self.match_cache:ensure(input .. ':' .. (self.resolved_completion_item and '1' or '0' .. ':') .. (matching_config.disallow_fuzzy_matching and '1' or '0') .. ':' .. (matching_config.disallow_partial_fuzzy_matching and '1' or '0') .. ':' .. (matching_config.disallow_partial_matching and '1' or '0') .. ':' .. (matching_config.disallow_prefix_unmatching and '1' or '0'), function() local option = { disallow_fuzzy_matching = matching_config.disallow_fuzzy_matching, + disallow_partial_fuzzy_matching = matching_config.disallow_partial_fuzzy_matching, disallow_partial_matching = matching_config.disallow_partial_matching, disallow_prefix_unmatching = matching_config.disallow_prefix_unmatching, synonyms = { @@ -360,27 +375,42 @@ entry.match = function(self, input, matching_config) }, } - local score, matches, _ - score, matches = matcher.match(input, self:get_filter_text(), option) + local score, matches, filter_text, _ + local checked = {} ---@type table + + filter_text = self:get_filter_text() + checked[filter_text] = true + score, matches = matcher.match(input, filter_text, option) -- Support the language server that doesn't respect VSCode's behaviors. + local prefix = '' if score == 0 then - if misc.safe(self:get_completion_item().textEdit) and not misc.empty(self:get_completion_item().textEdit.newText) then + if self:get_completion_item().textEdit and not misc.empty(self:get_completion_item().textEdit.newText) then local diff = self.source_offset - self:get_offset() if diff > 0 then - local prefix = string.sub(self.context.cursor_line, self:get_offset(), self:get_offset() + diff) - local accept = false + prefix = string.sub(self.context.cursor_line, self:get_offset(), self:get_offset() + diff) + local accept = nil accept = accept or string.match(prefix, '^[^%a]+$') accept = accept or string.find(self:get_completion_item().textEdit.newText, prefix, 1, true) if accept then - score, matches = matcher.match(input, prefix .. self:get_filter_text(), option) + filter_text = prefix .. self:get_filter_text() + if not checked[filter_text] then + checked[filter_text] = true + score, matches = matcher.match(input, filter_text, option) + end end end end end - if self:get_filter_text() ~= self:get_completion_item().label then - _, matches = matcher.match(input, self:get_completion_item().label, { self:get_word() }) + -- Fix highlight if filterText is not the same to vim_item.abbr. + if score > 0 then + local vim_item = self:get_vim_item(self.source_offset) + filter_text = vim_item.abbr or vim_item.word + if not checked[filter_text] then + local diff = self.source_offset - self:get_offset() + _, matches = matcher.match(input:sub(1 + diff), filter_text, option) + end end return { score = score, matches = matches } @@ -390,7 +420,7 @@ end ---Get resolved completion item if possible. ---@return lsp.CompletionItem entry.get_completion_item = function(self) - return self.cache:ensure({ 'get_completion_item', self.resolved_completion_item and 1 or 0 }, function() + return self.cache:ensure('get_completion_item', function() if self.resolved_completion_item then local completion_item = misc.copy(self.completion_item) for k, v in pairs(self.resolved_completion_item) do @@ -410,7 +440,7 @@ entry.get_documentation = function(self) local documents = {} -- detail - if misc.safe(item.detail) and item.detail ~= '' then + if item.detail and item.detail ~= '' then local ft = self.context.filetype local dot_index = string.find(ft, '%.') if dot_index ~= nil then @@ -422,13 +452,23 @@ entry.get_documentation = function(self) }) end - if type(item.documentation) == 'string' and item.documentation ~= '' then - table.insert(documents, { - kind = types.lsp.MarkupKind.PlainText, - value = str.trim(item.documentation), - }) - elseif type(item.documentation) == 'table' and item.documentation.value ~= '' then - table.insert(documents, item.documentation) + local documentation = item.documentation + if type(documentation) == 'string' and documentation ~= '' then + local value = str.trim(documentation) + if value ~= '' then + table.insert(documents, { + kind = types.lsp.MarkupKind.PlainText, + value = value, + }) + end + elseif type(documentation) == 'table' and not misc.empty(documentation.value) then + local value = str.trim(documentation.value) + if value ~= '' then + table.insert(documents, { + kind = documentation.kind, + value = value, + }) + end end return vim.lsp.util.convert_input_to_markdown_lines(documents) @@ -437,7 +477,7 @@ end ---Get completion item kind ---@return lsp.CompletionItemKind entry.get_kind = function(self) - return misc.safe(self:get_completion_item().kind) or types.lsp.CompletionItemKind.Text + return self:get_completion_item().kind or types.lsp.CompletionItemKind.Text end ---Execute completion item's command. @@ -457,7 +497,12 @@ entry.resolve = function(self, callback) if not self.resolving then self.resolving = true self.source:resolve(self.completion_item, function(completion_item) - self.resolved_completion_item = misc.safe(completion_item) or self.completion_item + self.resolving = false + if not completion_item then + return + end + self.resolved_completion_item = completion_item or self.completion_item + self.cache:clear() for _, c in ipairs(self.resolved_callbacks) do c() end @@ -465,4 +510,57 @@ entry.resolve = function(self, callback) end end +---@param completion_item lsp.CompletionItem +---@param defaults? lsp.internal.CompletionItemDefaults +---@return lsp.CompletionItem +entry.fill_defaults = function(_, completion_item, defaults) + defaults = defaults or {} + + if defaults.data then + completion_item.data = completion_item.data or defaults.data + end + + if defaults.commitCharacters then + completion_item.commitCharacters = completion_item.commitCharacters or defaults.commitCharacters + end + + if defaults.insertTextFormat then + completion_item.insertTextFormat = completion_item.insertTextFormat or defaults.insertTextFormat + end + + if defaults.insertTextMode then + completion_item.insertTextMode = completion_item.insertTextMode or defaults.insertTextMode + end + + if defaults.editRange then + if not completion_item.textEdit then + if defaults.editRange.insert then + completion_item.textEdit = { + insert = defaults.editRange.insert, + replace = defaults.editRange.replace, + newText = completion_item.textEditText or completion_item.label, + } + else + completion_item.textEdit = { + range = defaults.editRange, --[[@as lsp.Range]] + newText = completion_item.textEditText or completion_item.label, + } + end + end + end + + return completion_item +end + +---Convert the oneline range encoding. +entry.convert_range_encoding = function(self, range) + local from_encoding = self.source:get_position_encoding_kind() + return self.context.cache:ensure('entry.convert_range_encoding:' .. range.start.character .. ':' .. range['end'].character .. ':' .. from_encoding, function() + return { + start = types.lsp.Position.to_utf8(self.context.cursor_line, range.start, from_encoding), + ['end'] = types.lsp.Position.to_utf8(self.context.cursor_line, range['end'], from_encoding), + } + end) +end + return entry diff --git a/bundle/nvim-cmp/lua/cmp/entry_spec.lua b/bundle/nvim-cmp/lua/cmp/entry_spec.lua index d01125cac..3cb9b5861 100644 --- a/bundle/nvim-cmp/lua/cmp/entry_spec.lua +++ b/bundle/nvim-cmp/lua/cmp/entry_spec.lua @@ -1,6 +1,4 @@ local spec = require('cmp.utils.spec') -local source = require('cmp.source') -local async = require('cmp.utils.async') local entry = require('cmp.entry') @@ -290,41 +288,6 @@ describe('entry', function() assert.are.equal(e:get_vim_item(e:get_offset()).word, 'string') end) - it('[ansiblels] 1', function() - local item = { - detail = 'ansible.builtin', - filterText = 'blockinfile ansible.builtin.blockinfile', - kind = 7, - label = 'blockinfile', - sortText = '2_blockinfile', - textEdit = { - newText = '', - range = { - ['end'] = { - character = 7, - line = 15, - }, - start = { - character = 6, - line = 15, - }, - }, - }, - } - local s = source.new('dummy', { - resolve = function(_, _, callback) - item.textEdit.newText = 'modified' - callback(item) - end, - }) - local e = entry.new(spec.state('', 1, 1).manual(), s, item) - assert.are.equal(e:get_vim_item(e:get_offset()).word, 'blockinfile') - async.sync(function(done) - e:resolve(done) - end, 100) - assert.are.equal(e:get_vim_item(e:get_offset()).word, 'blockinfile') - end) - it('[#47] word should not contain \\n character', function() local state = spec.state('', 1, 1) @@ -339,4 +302,65 @@ describe('entry', function() assert.are.equal(e:get_vim_item(e:get_offset()).word, '__init__(self) -> None:') assert.are.equal(e:get_filter_text(), '__init__') end) + + -- I can't understand this test case... + -- it('[#1533] keyword pattern that include whitespace', function() + -- local state = spec.state(' ', 1, 2) + -- local state_source = state.source() + + -- state_source.get_keyword_pattern = function(_) + -- return '.' + -- end + + -- state.input(' ') + -- local e = entry.new(state.manual(), state_source, { + -- filterText = "constructor() {\n ... st = 'test';\n ", + -- kind = 1, + -- label = "constructor() {\n ... st = 'test';\n }", + -- textEdit = { + -- newText = "constructor() {\n this.test = 'test';\n }", + -- range = { + -- ['end'] = { + -- character = 2, + -- line = 2, + -- }, + -- start = { + -- character = 0, + -- line = 2, + -- }, + -- }, + -- }, + -- }) + -- assert.are.equal(e:get_offset(), 2) + -- assert.are.equal(e:get_vim_item(e:get_offset()).word, 'constructor() {') + -- end) + + it('[#1533] clang regression test', function() + local state = spec.state('jsonReader', 3, 11) + local state_source = state.source() + + state.input('.') + local e = entry.new(state.manual(), state_source, { + filterText = 'getPath()', + kind = 1, + label = 'getPath()', + textEdit = { + newText = 'getPath()', + range = { + ['end'] = { + character = 11, + col = 12, + line = 2, + row = 3, + }, + start = { + character = 11, + line = 2, + }, + }, + }, + }) + assert.are.equal(e:get_offset(), 12) + assert.are.equal(e:get_vim_item(e:get_offset()).word, 'getPath()') + end) end) diff --git a/bundle/nvim-cmp/lua/cmp/init.lua b/bundle/nvim-cmp/lua/cmp/init.lua index dbcd8668b..03722b65e 100644 --- a/bundle/nvim-cmp/lua/cmp/init.lua +++ b/bundle/nvim-cmp/lua/cmp/init.lua @@ -5,6 +5,7 @@ local feedkeys = require('cmp.utils.feedkeys') local autocmd = require('cmp.utils.autocmd') local keymap = require('cmp.utils.keymap') local misc = require('cmp.utils.misc') +local async = require('cmp.utils.async') local cmp = {} @@ -29,6 +30,7 @@ cmp.config.disable = misc.none cmp.config.compare = require('cmp.config.compare') cmp.config.sources = require('cmp.config.sources') cmp.config.mapping = require('cmp.config.mapping') +cmp.config.window = require('cmp.config.window') ---Sync asynchronous process. cmp.sync = function(callback) @@ -48,7 +50,7 @@ end ---Register completion sources ---@param name string ---@param s cmp.Source ----@return number +---@return integer cmp.register_source = function(name, s) local src = source.new(name, s) cmp.core:register_source(src) @@ -56,7 +58,7 @@ cmp.register_source = function(name, s) end ---Unregister completion source ----@param id number +---@param id integer cmp.unregister_source = function(id) cmp.core:unregister_source(id) end @@ -106,7 +108,6 @@ cmp.close = cmp.sync(function() if cmp.core.view:visible() then local release = cmp.core:suspend() cmp.core.view:close() - cmp.core:reset() vim.schedule(release) return true else @@ -129,6 +130,8 @@ end) ---Select next item if possible cmp.select_next_item = cmp.sync(function(option) option = option or {} + option.behavior = option.behavior or cmp.SelectBehavior.Insert + option.count = option.count or 1 if cmp.core.view:visible() then local release = cmp.core:suspend() @@ -136,11 +139,10 @@ cmp.select_next_item = cmp.sync(function(option) vim.schedule(release) return true elseif vim.fn.pumvisible() == 1 then - -- Special handling for native pum. Required to facilitate key mapping processing. - if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then - feedkeys.call(keymap.t(''), 'in') + if option.behavior == cmp.SelectBehavior.Insert then + feedkeys.call(keymap.t(string.rep('', option.count)), 'in') else - feedkeys.call(keymap.t(''), 'in') + feedkeys.call(keymap.t(string.rep('', option.count)), 'in') end return true end @@ -150,6 +152,8 @@ end) ---Select prev item if possible cmp.select_prev_item = cmp.sync(function(option) option = option or {} + option.behavior = option.behavior or cmp.SelectBehavior.Insert + option.count = option.count or 1 if cmp.core.view:visible() then local release = cmp.core:suspend() @@ -157,11 +161,10 @@ cmp.select_prev_item = cmp.sync(function(option) vim.schedule(release) return true elseif vim.fn.pumvisible() == 1 then - -- Special handling for native pum. Required to facilitate key mapping processing. - if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then - feedkeys.call(keymap.t(''), 'in') + if option.behavior == cmp.SelectBehavior.Insert then + feedkeys.call(keymap.t(string.rep('', option.count)), 'in') else - feedkeys.call(keymap.t(''), 'in') + feedkeys.call(keymap.t(string.rep('', option.count)), 'in') end return true end @@ -170,7 +173,7 @@ end) ---Scrolling documentation window if possible cmp.scroll_docs = cmp.sync(function(delta) - if cmp.core.view:visible() then + if cmp.core.view.docs_view:visible() then cmp.core.view:scroll_docs(delta) return true else @@ -181,25 +184,35 @@ end) ---Confirm completion cmp.confirm = cmp.sync(function(option, callback) option = option or {} + option.select = option.select or false + option.behavior = option.behavior or cmp.get_config().confirmation.default_behavior or cmp.ConfirmBehavior.Insert callback = callback or function() end - local e = cmp.core.view:get_selected_entry() or (option.select and cmp.core.view:get_first_entry() or nil) - if e then - cmp.core:confirm(e, { - behavior = option.behavior, - }, function() - callback() - cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.TriggerOnly })) - end) - return true - else - -- Special handling for native puma. Required to facilitate key mapping processing. - if vim.fn.complete_info({ 'selected' }).selected ~= -1 then - feedkeys.call(keymap.t(''), 'in') + if cmp.core.view:visible() then + local e = cmp.core.view:get_selected_entry() + if not e and option.select then + e = cmp.core.view:get_first_entry() + end + if e then + cmp.core:confirm(e, { + behavior = option.behavior, + }, function() + callback() + cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.TriggerOnly })) + end) + return true + end + elseif vim.fn.pumvisible() == 1 then + local index = vim.fn.complete_info({ 'selected' }).selected + if index == -1 and option.select then + index = 0 + end + if index ~= -1 then + vim.api.nvim_select_popupmenu_item(index, true, true, {}) return true end - return false end + return false end) ---Show status @@ -282,39 +295,28 @@ cmp.setup = setmetatable({ end, }) -autocmd.subscribe('InsertEnter', function() - feedkeys.call('', 'i', function() - if config.enabled() then - cmp.core:prepare() - cmp.core:on_change('InsertEnter') - end - end) -end) - -autocmd.subscribe('InsertLeave', function() - cmp.core:reset() - cmp.core.view:close() -end) - -autocmd.subscribe('CmdlineEnter', function() +-- In InsertEnter autocmd, vim will detects mode=normal unexpectedly. +local on_insert_enter = function() if config.enabled() then + cmp.config.compare.scopes:update() + cmp.config.compare.locality:update() cmp.core:prepare() cmp.core:on_change('InsertEnter') end -end) +end +autocmd.subscribe({ 'CmdlineEnter' }, async.debounce_next_tick(on_insert_enter)) +autocmd.subscribe({ 'InsertEnter' }, async.debounce_next_tick_by_keymap(on_insert_enter)) -autocmd.subscribe('CmdlineLeave', function() - cmp.core:reset() - cmp.core.view:close() -end) - -autocmd.subscribe('TextChanged', function() +-- async.throttle is needed for performance. The mapping `:...` will fire `CmdlineChanged` for each character. +local on_text_changed = function() if config.enabled() then cmp.core:on_change('TextChanged') end -end) +end +autocmd.subscribe({ 'TextChangedI', 'TextChangedP' }, on_text_changed) +autocmd.subscribe('CmdlineChanged', async.debounce_next_tick(on_text_changed)) -autocmd.subscribe('CursorMoved', function() +autocmd.subscribe('CursorMovedI', function() if config.enabled() then cmp.core:on_moved() else @@ -323,9 +325,10 @@ autocmd.subscribe('CursorMoved', function() end end) -autocmd.subscribe('InsertEnter', function() - cmp.config.compare.scopes:update() - cmp.config.compare.locality:update() +-- If make this asynchronous, the completion menu will not close when the command output is displayed. +autocmd.subscribe({ 'InsertLeave', 'CmdlineLeave' }, function() + cmp.core:reset() + cmp.core.view:close() end) cmp.event:on('complete_done', function(evt) diff --git a/bundle/nvim-cmp/lua/cmp/matcher.lua b/bundle/nvim-cmp/lua/cmp/matcher.lua index 7649f047b..26b3d1f27 100644 --- a/bundle/nvim-cmp/lua/cmp/matcher.lua +++ b/bundle/nvim-cmp/lua/cmp/matcher.lua @@ -66,14 +66,20 @@ end -- -- `candlesingle` -> candle#accept#single -- ^^^^^^~~~~~~ ^^^^^^ ~~~~~~ --- -- * The `accept`'s `a` should not match to `candle`'s `a` -- +-- 7. Avoid false positive matching +-- +-- `,` -> print, +-- ~ +-- * Typically, the middle match with symbol characters only is false positive. should be ignored. +-- +-- ---Match entry ---@param input string ---@param word string ----@param option { synonyms: string[], disallow_fuzzy_matching: boolean, disallow_partial_matching: boolean, disallow_prefix_unmatching: boolean } ----@return number +---@param option { synonyms: string[], disallow_fullfuzzy_matching: boolean, disallow_fuzzy_matching: boolean, disallow_partial_fuzzy_matching: boolean, disallow_partial_matching: boolean, disallow_prefix_unmatching: boolean } +---@return integer matcher.match = function(input, word, option) option = option or {} @@ -100,12 +106,14 @@ matcher.match = function(input, word, option) local input_end_index = 1 local word_index = 1 local word_bound_index = 1 + local no_symbol_match = false while input_end_index <= #input and word_index <= #word do local m = matcher.find_match_region(input, input_start_index, input_end_index, word, word_index) if m and input_end_index <= m.input_match_end then m.index = word_bound_index input_start_index = m.input_match_start + 1 input_end_index = m.input_match_end + 1 + no_symbol_match = no_symbol_match or m.no_symbol_match word_index = char.get_next_semantic_index(word, m.word_match_end) table.insert(matches, m) else @@ -120,6 +128,11 @@ matcher.match = function(input, word, option) end if #matches == 0 then + if not option.disallow_fuzzy_matching and not option.disallow_prefix_unmatching and not option.disallow_partial_fuzzy_matching then + if matcher.fuzzy(input, word, matches, option) then + return 1, matches + end + end return 0, {} end @@ -146,6 +159,10 @@ matcher.match = function(input, word, option) end end + if no_symbol_match and not prefix then + return 0, {} + end + -- Compute prefix match score local score = prefix and matcher.PREFIX_FACTOR or 0 local offset = prefix and matches[1].index - 1 or 0 @@ -167,8 +184,10 @@ matcher.match = function(input, word, option) -- Check remaining input as fuzzy if matches[#matches].input_match_end < #input then if not option.disallow_fuzzy_matching then - if prefix and matcher.fuzzy(input, word, matches) then - return score, matches + if not option.disallow_partial_fuzzy_matching or prefix then + if matcher.fuzzy(input, word, matches, option) then + return score, matches + end end end return 0, {} @@ -178,11 +197,10 @@ matcher.match = function(input, word, option) end --- fuzzy -matcher.fuzzy = function(input, word, matches) - local last_match = matches[#matches] +matcher.fuzzy = function(input, word, matches, option) + local input_index = matches[#matches] and (matches[#matches].input_match_end + 1) or 1 -- Lately specified middle of text. - local input_index = last_match.input_match_end + 1 for i = 1, #matches - 1 do local curr_match = matches[i] local next_match = matches[i + 1] @@ -200,10 +218,9 @@ matcher.fuzzy = function(input, word, matches) end -- Remaining text fuzzy match. - local last_input_index = input_index local matched = false local word_offset = 0 - local word_index = last_match.word_match_end + 1 + local word_index = matches[#matches] and (matches[#matches].word_match_end + 1) or 1 local input_match_start = -1 local input_match_end = -1 local word_match_start = -1 @@ -220,12 +237,26 @@ matcher.fuzzy = function(input, word, matches) input_index = input_index + 1 strict_count = strict_count + (c1 == c2 and 1 or 0) match_count = match_count + 1 - elseif matched then - input_index = last_input_index - input_match_end = input_index - 1 + else + if option.disallow_fullfuzzy_matching then + break + else + if matched then + table.insert(matches, { + input_match_start = input_match_start, + input_match_end = input_index - 1, + word_match_start = word_match_start, + word_match_end = word_index + word_offset - 1, + strict_ratio = strict_count / match_count, + fuzzy = true, + }) + end + end + matched = false end word_offset = word_offset + 1 end + if input_index > #input then table.insert(matches, { input_match_start = input_match_start, @@ -260,6 +291,7 @@ matcher.find_match_region = function(input, input_start_index, input_end_index, local word_offset = 0 local strict_count = 0 local match_count = 0 + local no_symbol_match = false while input_index <= #input and word_index + word_offset <= #word do local c1 = string.byte(input, input_index) local c2 = string.byte(word, word_index + word_offset) @@ -272,6 +304,7 @@ matcher.find_match_region = function(input, input_start_index, input_end_index, strict_count = strict_count + (c1 == c2 and 1 or 0) match_count = match_count + 1 word_offset = word_offset + 1 + no_symbol_match = no_symbol_match or char.is_symbol(c1) else -- Match end (partial region) if input_match_start ~= -1 then @@ -281,6 +314,7 @@ matcher.find_match_region = function(input, input_start_index, input_end_index, word_match_start = word_index, word_match_end = word_index + word_offset - 1, strict_ratio = strict_count / match_count, + no_symbol_match = no_symbol_match, fuzzy = false, } else @@ -298,6 +332,7 @@ matcher.find_match_region = function(input, input_start_index, input_end_index, word_match_start = word_index, word_match_end = word_index + word_offset - 1, strict_ratio = strict_count / match_count, + no_symbol_match = no_symbol_match, fuzzy = false, } end diff --git a/bundle/nvim-cmp/lua/cmp/matcher_spec.lua b/bundle/nvim-cmp/lua/cmp/matcher_spec.lua index 768988e84..cb753df92 100644 --- a/bundle/nvim-cmp/lua/cmp/matcher_spec.lua +++ b/bundle/nvim-cmp/lua/cmp/matcher_spec.lua @@ -28,8 +28,35 @@ describe('matcher', function() assert.is.truthy(matcher.match('my_', 'my_awesome_variable') > matcher.match('my_', 'completion_matching_strategy_list')) assert.is.truthy(matcher.match('2', '[[2021') >= 1) + assert.is.truthy(matcher.match(',', 'pri,') == 0) + assert.is.truthy(matcher.match('/', '/**') >= 1) + assert.is.truthy(matcher.match('true', 'v:true', { synonyms = { 'true' } }) == matcher.match('true', 'true')) assert.is.truthy(matcher.match('g', 'get', { synonyms = { 'get' } }) > matcher.match('g', 'dein#get', { 'dein#get' })) + + assert.is.truthy(matcher.match('Unit', 'net.UnixListener', { disallow_partial_fuzzy_matching = true }) == 0) + assert.is.truthy(matcher.match('Unit', 'net.UnixListener', { disallow_partial_fuzzy_matching = false }) >= 1) + + assert.is.truthy(matcher.match('emg', 'error_msg') >= 1) + assert.is.truthy(matcher.match('sasr', 'saved_splitright') >= 1) + + local score, matches + score, matches = matcher.match('tail', 'HCDetails', { + disallow_fuzzy_matching = false, + disallow_partial_matching = false, + disallow_prefix_unmatching = false, + disallow_partial_fuzzy_matching = false, + }) + assert.is.truthy(score >= 1) + assert.equals(matches[1].word_match_start, 5) + + score = matcher.match('tail', 'HCDetails', { + disallow_fuzzy_matching = false, + disallow_partial_matching = false, + disallow_prefix_unmatching = false, + disallow_partial_fuzzy_matching = true, + }) + assert.is.truthy(score == 0) end) it('disallow_fuzzy_matching', function() @@ -37,6 +64,11 @@ describe('matcher', function() assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = false }) >= 1) end) + it('disallow_fullfuzzy_matching', function() + assert.is.truthy(matcher.match('svd', 'saved_splitright', { disallow_fullfuzzy_matching = true }) == 0) + assert.is.truthy(matcher.match('svd', 'saved_splitright', { disallow_fullfuzzy_matching = false }) >= 1) + end) + it('disallow_partial_matching', function() assert.is.truthy(matcher.match('fb', 'foo_bar', { disallow_partial_matching = true }) == 0) assert.is.truthy(matcher.match('fb', 'foo_bar', { disallow_partial_matching = false }) >= 1) diff --git a/bundle/nvim-cmp/lua/cmp/source.lua b/bundle/nvim-cmp/lua/cmp/source.lua index b73d43e05..9b8f662ce 100644 --- a/bundle/nvim-cmp/lua/cmp/source.lua +++ b/bundle/nvim-cmp/lua/cmp/source.lua @@ -10,23 +10,23 @@ local pattern = require('cmp.utils.pattern') local char = require('cmp.utils.char') ---@class cmp.Source ----@field public id number +---@field public id integer ---@field public name string ---@field public source any ---@field public cache cmp.Cache ----@field public revision number +---@field public revision integer ---@field public incomplete boolean ---@field public is_triggered_by_symbol boolean ---@field public entries cmp.Entry[] ----@field public offset number ----@field public request_offset number +---@field public offset integer +---@field public request_offset integer ---@field public context cmp.Context ---@field public completion_context lsp.CompletionContext|nil ---@field public status cmp.SourceStatus ---@field public complete_dedup function local source = {} ----@alias cmp.SourceStatus "1" | "2" | "3" +---@alias cmp.SourceStatus 1 | 2 | 3 source.SourceStatus = {} source.SourceStatus.WAITING = 1 source.SourceStatus.FETCHING = 2 @@ -46,7 +46,6 @@ source.new = function(name, s) end ---Reset current completion state ----@return boolean source.reset = function(self) self.cache:clear() self.revision = self.revision + 1 @@ -89,86 +88,92 @@ source.get_entries = function(self, ctx) return {} end - local target_entries = (function() - local key = { 'get_entries', self.revision } - for i = ctx.cursor.col, self.offset, -1 do - key[3] = string.sub(ctx.cursor_before_line, 1, i) - local prev_entries = self.cache:get(key) - if prev_entries then - return prev_entries - end + local target_entries = self.entries + + local prev = self.cache:get({ 'get_entries', tostring(self.revision) }) + if prev and ctx.cursor.row == prev.ctx.cursor.row and self.offset == prev.offset then + if ctx.cursor.col == prev.ctx.cursor.col then + return prev.entries end - return self.entries - end)() + -- only use prev entries when cursor is moved forward. + -- and the pattern offset is the same. + if prev.ctx.cursor.col <= ctx.cursor.col then + target_entries = prev.entries + end + end + + local entry_filter = self:get_entry_filter() local inputs = {} + ---@type cmp.Entry[] local entries = {} + local matching_config = self:get_matching_config() for _, e in ipairs(target_entries) do local o = e:get_offset() if not inputs[o] then inputs[o] = string.sub(ctx.cursor_before_line, o) end - local match = e:match(inputs[o], self:get_matching_config()) + local match = e:match(inputs[o], matching_config) e.score = match.score e.exact = false if e.score >= 1 then e.matches = match.matches e.exact = e:get_filter_text() == inputs[o] or e:get_word() == inputs[o] - table.insert(entries, e) - end - end - self.cache:set({ 'get_entries', self.revision, ctx.cursor_before_line }, entries) - local max_item_count = self:get_source_config().max_item_count or 200 - local limited_entries = {} - for _, e in ipairs(entries) do - table.insert(limited_entries, e) - if max_item_count and #limited_entries >= max_item_count then - break + if entry_filter(e, ctx) then + entries[#entries + 1] = e + end + end + async.yield() + if ctx.aborted then + async.abort() end end - return limited_entries + + self.cache:set({ 'get_entries', tostring(self.revision) }, { entries = entries, ctx = ctx, offset = self.offset }) + + return entries end ----Get default insert range ----@return lsp.Range|nil +---Get default insert range (UTF8 byte index). +---@return lsp.Range source.get_default_insert_range = function(self) if not self.context then - return nil + error('context is not initialized yet.') end - return self.cache:ensure({ 'get_default_insert_range', self.revision }, function() + return self.cache:ensure({ 'get_default_insert_range', tostring(self.revision) }, function() return { start = { line = self.context.cursor.row - 1, - character = misc.to_utfindex(self.context.cursor_line, self.offset), + character = self.offset - 1, }, ['end'] = { line = self.context.cursor.row - 1, - character = misc.to_utfindex(self.context.cursor_line, self.context.cursor.col), + character = self.context.cursor.col - 1, }, } end) end ----Get default replace range ----@return lsp.Range|nil +---Get default replace range (UTF8 byte index). +---@return lsp.Range source.get_default_replace_range = function(self) if not self.context then - return nil + error('context is not initialized yet.') end - return self.cache:ensure({ 'get_default_replace_range', self.revision }, function() + return self.cache:ensure({ 'get_default_replace_range', tostring(self.revision) }, function() local _, e = pattern.offset('^' .. '\\%(' .. self:get_keyword_pattern() .. '\\)', string.sub(self.context.cursor_line, self.offset)) return { start = { line = self.context.cursor.row - 1, - character = misc.to_utfindex(self.context.cursor_line, self.offset), + character = self.offset, }, ['end'] = { line = self.context.cursor.row - 1, - character = misc.to_utfindex(self.context.cursor_line, e and self.offset + e - 1 or self.context.cursor.col), + character = (e and self.offset + e - 2 or self.context.cursor.col - 1), }, } end) @@ -217,13 +222,16 @@ source.get_keyword_pattern = function(self) return c.keyword_pattern end if self.source.get_keyword_pattern then - return self.source:get_keyword_pattern(misc.copy(c)) + local keyword_pattern = self.source:get_keyword_pattern(misc.copy(c)) + if keyword_pattern then + return keyword_pattern + end end return config.get().completion.keyword_pattern end ---Get keyword_length ----@return number +---@return integer source.get_keyword_length = function(self) local c = self:get_source_config() if c.keyword_length then @@ -232,10 +240,31 @@ source.get_keyword_length = function(self) return config.get().completion.keyword_length or 1 end +---Get filter +--@return fun(entry: cmp.Entry, context: cmp.Context): boolean +source.get_entry_filter = function(self) + local c = self:get_source_config() + if c.entry_filter then + return c.entry_filter --[[@as fun(entry: cmp.Entry, context: cmp.Context): boolean]] + end + return function(_, _) + return true + end +end + +---Get lsp.PositionEncodingKind +---@return lsp.PositionEncodingKind +source.get_position_encoding_kind = function(self) + if self.source.get_position_encoding_kind then + return self.source:get_position_encoding_kind() + end + return types.lsp.PositionEncodingKind.UTF16 +end + ---Invoke completion ---@param ctx cmp.Context ---@param callback function ----@return boolean Return true if not trigger completion. +---@return boolean? Return true if not trigger completion. source.complete = function(self, ctx, callback) local offset = ctx:get_offset(self:get_keyword_pattern()) @@ -260,7 +289,7 @@ source.complete = function(self, ctx, callback) triggerCharacter = before_char, } elseif ctx:get_reason() ~= types.cmp.ContextReason.TriggerOnly then - if self:get_keyword_length() <= (ctx.cursor.col - offset) then + if offset < ctx.cursor.col and self:get_keyword_length() <= (ctx.cursor.col - offset) then if self.incomplete and self.context.cursor.col ~= ctx.cursor.col and self.status ~= source.SourceStatus.FETCHING then completion_context = { triggerKind = types.lsp.CompletionTriggerKind.TriggerForIncompleteCompletions, @@ -300,6 +329,10 @@ source.complete = function(self, ctx, callback) completion_context = completion_context, }), self.complete_dedup(vim.schedule_wrap(function(response) + if self.context ~= ctx then + return + end + ---@type lsp.CompletionResponse response = response or {} self.incomplete = response.isIncomplete or false @@ -312,14 +345,14 @@ source.complete = function(self, ctx, callback) self.status = source.SourceStatus.COMPLETED self.entries = {} for i, item in ipairs(response.items or response) do - if (misc.safe(item) or {}).label then - local e = entry.new(ctx, self, item) + if (item or {}).label then + local e = entry.new(ctx, self, item, response.itemDefaults) self.entries[i] = e self.offset = math.min(self.offset, e:get_offset()) end end self.revision = self.revision + 1 - if #self:get_entries(ctx) == 0 then + if #self.entries == 0 then self.offset = old_offset self.entries = old_entries self.revision = self.revision + 1 diff --git a/bundle/nvim-cmp/lua/cmp/types/cmp.lua b/bundle/nvim-cmp/lua/cmp/types/cmp.lua index b4e940273..e3bf8e6e3 100644 --- a/bundle/nvim-cmp/lua/cmp/types/cmp.lua +++ b/bundle/nvim-cmp/lua/cmp/types/cmp.lua @@ -1,37 +1,43 @@ local cmp = {} ----@alias cmp.ConfirmBehavior "'insert'" | "'replace'" -cmp.ConfirmBehavior = {} -cmp.ConfirmBehavior.Insert = 'insert' -cmp.ConfirmBehavior.Replace = 'replace' +---@alias cmp.ConfirmBehavior 'insert' | 'replace' +cmp.ConfirmBehavior = { + Insert = 'insert', + Replace = 'replace', +} ----@alias cmp.SelectBehavior "'insert'" | "'select'" -cmp.SelectBehavior = {} -cmp.SelectBehavior.Insert = 'insert' -cmp.SelectBehavior.Select = 'select' +---@alias cmp.SelectBehavior 'insert' | 'select' +cmp.SelectBehavior = { + Insert = 'insert', + Select = 'select', +} ----@alias cmp.ContextReason "'auto'" | "'manual'" | "'none'" -cmp.ContextReason = {} -cmp.ContextReason.Auto = 'auto' -cmp.ContextReason.Manual = 'manual' -cmp.ContextReason.TriggerOnly = 'triggerOnly' -cmp.ContextReason.None = 'none' +---@alias cmp.ContextReason 'auto' | 'manual' | 'triggerOnly' | 'none' +cmp.ContextReason = { + Auto = 'auto', + Manual = 'manual', + TriggerOnly = 'triggerOnly', + None = 'none', +} ----@alias cmp.TriggerEvent "'InsertEnter'" | "'TextChanged'" -cmp.TriggerEvent = {} -cmp.TriggerEvent.InsertEnter = 'InsertEnter' -cmp.TriggerEvent.TextChanged = 'TextChanged' +---@alias cmp.TriggerEvent 'InsertEnter' | 'TextChanged' +cmp.TriggerEvent = { + InsertEnter = 'InsertEnter', + TextChanged = 'TextChanged', +} ----@alias cmp.PreselectMode "'item'" | "'None'" -cmp.PreselectMode = {} -cmp.PreselectMode.Item = 'item' -cmp.PreselectMode.None = 'none' +---@alias cmp.PreselectMode 'item' | 'None' +cmp.PreselectMode = { + Item = 'item', + None = 'none', +} ----@alias cmp.ItemField "'abbr'" | "'kind'" | "'menu'" -cmp.ItemField = {} -cmp.ItemField.Abbr = 'abbr' -cmp.ItemField.Kind = 'kind' -cmp.ItemField.Menu = 'menu' +---@alias cmp.ItemField 'abbr' | 'kind' | 'menu' +cmp.ItemField = { + Abbr = 'abbr', + Kind = 'kind', + Menu = 'menu', +} ---@class cmp.ContextOption ---@field public reason cmp.ContextReason|nil @@ -45,22 +51,24 @@ cmp.ItemField.Menu = 'menu' ---@class cmp.SnippetExpansionParams ---@field public body string ----@field public insert_text_mode number +---@field public insert_text_mode integer ---@class cmp.CompleteParams ---@field public reason? cmp.ContextReason ---@field public config? cmp.ConfigSchema ----@class cmp.Setup ----@field public __call fun(c: cmp.ConfigSchema) +---@class cmp.SetupProperty ---@field public buffer fun(c: cmp.ConfigSchema) ---@field public global fun(c: cmp.ConfigSchema) ----@field public cmdline fun(type: string, c: cmp.ConfigSchema) +---@field public cmdline fun(type: string|string[], c: cmp.ConfigSchema) +---@field public filetype fun(type: string|string[], c: cmp.ConfigSchema) + +---@alias cmp.Setup cmp.SetupProperty | fun(c: cmp.ConfigSchema) ---@class cmp.SourceApiParams: cmp.SourceConfig ---@class cmp.SourceCompletionApiParams : cmp.SourceConfig ----@field public offset number +---@field public offset integer ---@field public context cmp.Context ---@field public completion_context lsp.CompletionContext @@ -71,11 +79,12 @@ cmp.ItemField.Menu = 'menu' ---@field public s nil|function(fallback: function): void ---@class cmp.ConfigSchema ----@field private revision number ----@field public enabled fun():boolean|boolean +---@field private revision integer +---@field public enabled boolean | fun(): boolean +---@field public performance cmp.PerformanceConfig ---@field public preselect cmp.PreselectMode ---@field public completion cmp.CompletionConfig ----@field public documentation cmp.DocumentationConfig|"false" +---@field public window cmp.WindowConfig|nil ---@field public confirmation cmp.ConfirmationConfig ---@field public matching cmp.MatchingConfig ---@field public sorting cmp.SortingConfig @@ -86,19 +95,32 @@ cmp.ItemField.Menu = 'menu' ---@field public view cmp.ViewConfig ---@field public experimental cmp.ExperimentalConfig +---@class cmp.PerformanceConfig +---@field public debounce integer +---@field public throttle integer +---@field public fetching_timeout integer +---@field public async_budget integer Maximum time (in ms) an async function is allowed to run during one step of the event loop. +---@field public max_view_entries integer + +---@class cmp.WindowConfig +---@field completion cmp.WindowConfig +---@field documentation cmp.WindowConfig|nil + ---@class cmp.CompletionConfig ---@field public autocomplete cmp.TriggerEvent[] ---@field public completeopt string ----@field public keyword_pattern string ----@field public keyword_length number ---@field public get_trigger_characters fun(trigger_characters: string[]): string[] +---@field public keyword_length integer +---@field public keyword_pattern string ----@class cmp.DocumentationConfig ----@field public border string[] +---@class cmp.WindowConfig +---@field public border string|string[] ---@field public winhighlight string ----@field public maxwidth number|nil ----@field public maxheight number|nil ----@field public zindex number|nil +---@field public zindex integer|nil +---@field public max_width integer|nil +---@field public max_height integer|nil +---@field public scrolloff integer|nil +---@field public scrollbar boolean|true ---@class cmp.ConfirmationConfig ---@field public default_behavior cmp.ConfirmBehavior @@ -106,23 +128,25 @@ cmp.ItemField.Menu = 'menu' ---@class cmp.MatchingConfig ---@field public disallow_fuzzy_matching boolean +---@field public disallow_fullfuzzy_matching boolean +---@field public disallow_partial_fuzzy_matching boolean ---@field public disallow_partial_matching boolean ---@field public disallow_prefix_unmatching boolean ---@class cmp.SortingConfig ----@field public priority_weight number +---@field public priority_weight integer ---@field public comparators function[] ---@class cmp.FormattingConfig ---@field public fields cmp.ItemField[] +---@field public expandable_indicator boolean ---@field public format fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem ---@class cmp.SnippetConfig ---@field public expand fun(args: cmp.SnippetExpansionParams) ---@class cmp.ExperimentalConfig ----@field public native_menu boolean ----@field public ghost_text cmp.GhostTextConfig|"false" +---@field public ghost_text cmp.GhostTextConfig|false ---@class cmp.GhostTextConfig ---@field hl_group string @@ -130,12 +154,12 @@ cmp.ItemField.Menu = 'menu' ---@class cmp.SourceConfig ---@field public name string ---@field public option table|nil ----@field public priority number|nil +---@field public priority integer|nil ---@field public trigger_characters string[]|nil ---@field public keyword_pattern string|nil ----@field public keyword_length number|nil ----@field public max_item_count number|nil ----@field public group_index number|nil +---@field public keyword_length integer|nil +---@field public group_index integer|nil +---@field public entry_filter nil|function(entry: cmp.Entry, ctx: cmp.Context): boolean ---@class cmp.ViewConfig ---@field public entries cmp.EntriesConfig @@ -143,14 +167,14 @@ cmp.ItemField.Menu = 'menu' ---@alias cmp.EntriesConfig cmp.CustomEntriesConfig|cmp.NativeEntriesConfig|cmp.WildmenuEntriesConfig|string ---@class cmp.CustomEntriesConfig ----@field name "'custom'" ----@field selection_order "'top_down'"|"'near_cursor'" +---@field name 'custom' +---@field selection_order 'top_down'|'near_cursor' ---@class cmp.NativeEntriesConfig ----@field name "'native'" +---@field name 'native' ---@class cmp.WildmenuEntriesConfig ----@field name "'wildmenu'" +---@field name 'wildmenu' ---@field separator string|nil return cmp diff --git a/bundle/nvim-cmp/lua/cmp/types/lsp.lua b/bundle/nvim-cmp/lua/cmp/types/lsp.lua index c8a04473d..a62e0e7e9 100644 --- a/bundle/nvim-cmp/lua/cmp/types/lsp.lua +++ b/bundle/nvim-cmp/lua/cmp/types/lsp.lua @@ -1,151 +1,234 @@ local misc = require('cmp.utils.misc') + ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/ ---@class lsp local lsp = {} -lsp.Position = {} +---@enum lsp.PositionEncodingKind +lsp.PositionEncodingKind = { + UTF8 = 'utf-8', + UTF16 = 'utf-16', + UTF32 = 'utf-32', +} ----Convert lsp.Position to vim.Position ----@param buf number|string ----@param position lsp.Position ----@return vim.Position -lsp.Position.to_vim = function(buf, position) - if not vim.api.nvim_buf_is_loaded(buf) then - vim.fn.bufload(buf) - end - local lines = vim.api.nvim_buf_get_lines(buf, position.line, position.line + 1, false) - if #lines > 0 then +lsp.Position = { + ---Convert lsp.Position to vim.Position + ---@param buf integer + ---@param position lsp.Position + -- + ---@return vim.Position + to_vim = function(buf, position) + if not vim.api.nvim_buf_is_loaded(buf) then + vim.fn.bufload(buf) + end + local lines = vim.api.nvim_buf_get_lines(buf, position.line, position.line + 1, false) + if #lines > 0 then + return { + row = position.line + 1, + col = misc.to_vimindex(lines[1], position.character), + } + end return { row = position.line + 1, - col = misc.to_vimindex(lines[1], position.character), + col = position.character + 1, } - end - return { - row = position.line + 1, - col = position.character + 1, - } -end - ----Convert vim.Position to lsp.Position ----@param buf number|string ----@param position vim.Position ----@return lsp.Position -lsp.Position.to_lsp = function(buf, position) - if not vim.api.nvim_buf_is_loaded(buf) then - vim.fn.bufload(buf) - end - local lines = vim.api.nvim_buf_get_lines(buf, position.row - 1, position.row, false) - if #lines > 0 then + end, + ---Convert vim.Position to lsp.Position + ---@param buf integer + ---@param position vim.Position + ---@return lsp.Position + to_lsp = function(buf, position) + if not vim.api.nvim_buf_is_loaded(buf) then + vim.fn.bufload(buf) + end + local lines = vim.api.nvim_buf_get_lines(buf, position.row - 1, position.row, false) + if #lines > 0 then + return { + line = position.row - 1, + character = misc.to_utfindex(lines[1], position.col), + } + end return { line = position.row - 1, - character = misc.to_utfindex(lines[1], position.col), + character = position.col - 1, } - end - return { - line = position.row - 1, - character = position.col - 1, - } -end + end, -lsp.Range = {} + ---Convert position to utf8 from specified encoding. + ---@param text string + ---@param position lsp.Position + ---@param from_encoding? lsp.PositionEncodingKind + ---@return lsp.Position + to_utf8 = function(text, position, from_encoding) + from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16 + if from_encoding == lsp.PositionEncodingKind.UTF8 then + return position + end ----Convert lsp.Range to vim.Range ----@param buf number|string ----@param range lsp.Range ----@return vim.Range -lsp.Range.to_vim = function(buf, range) - return { - start = lsp.Position.to_vim(buf, range.start), - ['end'] = lsp.Position.to_vim(buf, range['end']), - } -end + local ok, byteindex = pcall(function() + return vim.str_byteindex(text, position.character, from_encoding == lsp.PositionEncodingKind.UTF16) + end) + if not ok then + return position + end + return { line = position.line, character = byteindex } + end, ----Convert vim.Range to lsp.Range ----@param buf number|string ----@param range vim.Range ----@return lsp.Range -lsp.Range.to_lsp = function(buf, range) - return { - start = lsp.Position.to_lsp(buf, range.start), - ['end'] = lsp.Position.to_lsp(buf, range['end']), - } -end + ---Convert position to utf16 from specified encoding. + ---@param text string + ---@param position lsp.Position + ---@param from_encoding? lsp.PositionEncodingKind + ---@return lsp.Position + to_utf16 = function(text, position, from_encoding) + from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16 + if from_encoding == lsp.PositionEncodingKind.UTF16 then + return position + end ----@alias lsp.CompletionTriggerKind "1" | "2" | "3" -lsp.CompletionTriggerKind = {} -lsp.CompletionTriggerKind.Invoked = 1 -lsp.CompletionTriggerKind.TriggerCharacter = 2 -lsp.CompletionTriggerKind.TriggerForIncompleteCompletions = 3 + local utf8 = lsp.Position.to_utf8(text, position, from_encoding) + for index = utf8.character, 0, -1 do + local ok, utf16index = pcall(function() + return select(2, vim.str_utfindex(text, index)) + end) + if ok then + return { line = utf8.line, character = utf16index } + end + end + return position + end, + + ---Convert position to utf32 from specified encoding. + ---@param text string + ---@param position lsp.Position + ---@param from_encoding? lsp.PositionEncodingKind + ---@return lsp.Position + to_utf32 = function(text, position, from_encoding) + from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16 + if from_encoding == lsp.PositionEncodingKind.UTF32 then + return position + end + + local utf8 = lsp.Position.to_utf8(text, position, from_encoding) + for index = utf8.character, 0, -1 do + local ok, utf32index = pcall(function() + return select(1, vim.str_utfindex(text, index)) + end) + if ok then + return { line = utf8.line, character = utf32index } + end + end + return position + end, +} + +lsp.Range = { + ---Convert lsp.Range to vim.Range + ---@param buf integer + ---@param range lsp.Range + ---@return vim.Range + to_vim = function(buf, range) + return { + start = lsp.Position.to_vim(buf, range.start), + ['end'] = lsp.Position.to_vim(buf, range['end']), + } + end, + + ---Convert vim.Range to lsp.Range + ---@param buf integer + ---@param range vim.Range + ---@return lsp.Range + to_lsp = function(buf, range) + return { + start = lsp.Position.to_lsp(buf, range.start), + ['end'] = lsp.Position.to_lsp(buf, range['end']), + } + end, +} + +---@alias lsp.CompletionTriggerKind 1 | 2 | 3 +lsp.CompletionTriggerKind = { + Invoked = 1, + TriggerCharacter = 2, + TriggerForIncompleteCompletions = 3, +} + +---@alias lsp.InsertTextFormat 1 | 2 +lsp.InsertTextFormat = {} +lsp.InsertTextFormat.PlainText = 1 +lsp.InsertTextFormat.Snippet = 2 + +---@alias lsp.InsertTextMode 1 | 2 +lsp.InsertTextMode = { + AsIs = 1, + AdjustIndentation = 2, +} + +---@alias lsp.MarkupKind 'plaintext' | 'markdown' +lsp.MarkupKind = { + PlainText = 'plaintext', + Markdown = 'markdown', +} + +---@alias lsp.CompletionItemTag 1 +lsp.CompletionItemTag = { + Deprecated = 1, +} + +---@alias lsp.CompletionItemKind 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 +lsp.CompletionItemKind = { + Text = 1, + Method = 2, + Function = 3, + Constructor = 4, + Field = 5, + Variable = 6, + Class = 7, + Interface = 8, + Module = 9, + Property = 10, + Unit = 11, + Value = 12, + Enum = 13, + Keyword = 14, + Snippet = 15, + Color = 16, + File = 17, + Reference = 18, + Folder = 19, + EnumMember = 20, + Constant = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, +} +lsp.CompletionItemKind = vim.tbl_add_reverse_lookup(lsp.CompletionItemKind) + +---@class lsp.internal.CompletionItemDefaults +---@field public commitCharacters? string[] +---@field public editRange? lsp.Range | { insert: lsp.Range, replace: lsp.Range } +---@field public insertTextFormat? lsp.InsertTextFormat +---@field public insertTextMode? lsp.InsertTextMode +---@field public data? any ---@class lsp.CompletionContext ---@field public triggerKind lsp.CompletionTriggerKind ---@field public triggerCharacter string|nil ----@alias lsp.InsertTextFormat "1" | "2" -lsp.InsertTextFormat = {} -lsp.InsertTextFormat.PlainText = 1 -lsp.InsertTextFormat.Snippet = 2 -lsp.InsertTextFormat = vim.tbl_add_reverse_lookup(lsp.InsertTextFormat) - ----@alias lsp.InsertTextMode "1" | "2" -lsp.InsertTextMode = {} -lsp.InsertTextMode.AsIs = 0 -lsp.InsertTextMode.AdjustIndentation = 1 -lsp.InsertTextMode = vim.tbl_add_reverse_lookup(lsp.InsertTextMode) - ----@alias lsp.MarkupKind "'plaintext'" | "'markdown'" -lsp.MarkupKind = {} -lsp.MarkupKind.PlainText = 'plaintext' -lsp.MarkupKind.Markdown = 'markdown' -lsp.MarkupKind = vim.tbl_add_reverse_lookup(lsp.MarkupKind) - ----@alias lsp.CompletionItemTag "1" -lsp.CompletionItemTag = {} -lsp.CompletionItemTag.Deprecated = 1 -lsp.CompletionItemTag = vim.tbl_add_reverse_lookup(lsp.CompletionItemTag) - ----@alias lsp.CompletionItemKind "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" | "12" | "13" | "14" | "15" | "16" | "17" | "18" | "19" | "20" | "21" | "22" | "23" | "24" | "25" -lsp.CompletionItemKind = {} -lsp.CompletionItemKind.Text = 1 -lsp.CompletionItemKind.Method = 2 -lsp.CompletionItemKind.Function = 3 -lsp.CompletionItemKind.Constructor = 4 -lsp.CompletionItemKind.Field = 5 -lsp.CompletionItemKind.Variable = 6 -lsp.CompletionItemKind.Class = 7 -lsp.CompletionItemKind.Interface = 8 -lsp.CompletionItemKind.Module = 9 -lsp.CompletionItemKind.Property = 10 -lsp.CompletionItemKind.Unit = 11 -lsp.CompletionItemKind.Value = 12 -lsp.CompletionItemKind.Enum = 13 -lsp.CompletionItemKind.Keyword = 14 -lsp.CompletionItemKind.Snippet = 15 -lsp.CompletionItemKind.Color = 16 -lsp.CompletionItemKind.File = 17 -lsp.CompletionItemKind.Reference = 18 -lsp.CompletionItemKind.Folder = 19 -lsp.CompletionItemKind.EnumMember = 20 -lsp.CompletionItemKind.Constant = 21 -lsp.CompletionItemKind.Struct = 22 -lsp.CompletionItemKind.Event = 23 -lsp.CompletionItemKind.Operator = 24 -lsp.CompletionItemKind.TypeParameter = 25 -lsp.CompletionItemKind = vim.tbl_add_reverse_lookup(lsp.CompletionItemKind) - ---@class lsp.CompletionList ---@field public isIncomplete boolean +---@field public itemDefaults? lsp.internal.CompletionItemDefaults ---@field public items lsp.CompletionItem[] ----@alias lsp.CompletionResponse lsp.CompletionList|lsp.CompletionItem[]|nil +---@alias lsp.CompletionResponse lsp.CompletionList|lsp.CompletionItem[] ---@class lsp.MarkupContent ---@field public kind lsp.MarkupKind ---@field public value string ---@class lsp.Position ----@field public line number ----@field public character number +---@field public line integer +---@field public character integer ---@class lsp.Range ---@field public start lsp.Position @@ -160,34 +243,45 @@ lsp.CompletionItemKind = vim.tbl_add_reverse_lookup(lsp.CompletionItemKind) ---@field public range lsp.Range|nil ---@field public newText string ----@class lsp.InsertReplaceTextEdit ----@field public insert lsp.Range|nil ----@field public replace lsp.Range|nil +---@alias lsp.InsertReplaceTextEdit lsp.internal.InsertTextEdit|lsp.internal.ReplaceTextEdit + +---@class lsp.internal.InsertTextEdit +---@field public insert lsp.Range +---@field public newText string + +---@class lsp.internal.ReplaceTextEdit +---@field public replace lsp.Range ---@field public newText string ---@class lsp.CompletionItemLabelDetails ----@field public detail string|nil ----@field public description string|nil +---@field public detail? string +---@field public description? string + +---@class lsp.internal.CmpCompletionExtension +---@field public kind_text string +---@field public kind_hl_group string ---@class lsp.CompletionItem ---@field public label string ----@field public labelDetails lsp.CompletionItemLabelDetails|nil ----@field public kind lsp.CompletionItemKind|nil ----@field public tags lsp.CompletionItemTag[]|nil ----@field public detail string|nil ----@field public documentation lsp.MarkupContent|string|nil ----@field public deprecated boolean|nil ----@field public preselect boolean|nil ----@field public sortText string|nil ----@field public filterText string|nil ----@field public insertText string|nil ----@field public insertTextFormat lsp.InsertTextFormat ----@field public insertTextMode lsp.InsertTextMode ----@field public textEdit lsp.TextEdit|lsp.InsertReplaceTextEdit|nil ----@field public additionalTextEdits lsp.TextEdit[] ----@field public commitCharacters string[]|nil ----@field public command lsp.Command|nil ----@field public data any|nil +---@field public labelDetails? lsp.CompletionItemLabelDetails +---@field public kind? lsp.CompletionItemKind +---@field public tags? lsp.CompletionItemTag[] +---@field public detail? string +---@field public documentation? lsp.MarkupContent|string +---@field public deprecated? boolean +---@field public preselect? boolean +---@field public sortText? string +---@field public filterText? string +---@field public insertText? string +---@field public insertTextFormat? lsp.InsertTextFormat +---@field public insertTextMode? lsp.InsertTextMode +---@field public textEdit? lsp.TextEdit|lsp.InsertReplaceTextEdit +---@field public textEditText? string +---@field public additionalTextEdits? lsp.TextEdit[] +---@field public commitCharacters? string[] +---@field public command? lsp.Command +---@field public data? any +---@field public cmp? lsp.internal.CmpCompletionExtension --- ---TODO: Should send the issue for upstream? ---@field public word string|nil diff --git a/bundle/nvim-cmp/lua/cmp/types/vim.lua b/bundle/nvim-cmp/lua/cmp/types/vim.lua index 0ae28c69f..8532534c6 100644 --- a/bundle/nvim-cmp/lua/cmp/types/vim.lua +++ b/bundle/nvim-cmp/lua/cmp/types/vim.lua @@ -3,17 +3,17 @@ ---@field public abbr string|nil ---@field public kind string|nil ---@field public menu string|nil ----@field public equal "1"|nil ----@field public empty "1"|nil ----@field public dup "1"|nil +---@field public equal 1|nil +---@field public empty 1|nil +---@field public dup 1|nil ---@field public id any ---@field public abbr_hl_group string|nil ---@field public kind_hl_group string|nil ---@field public menu_hl_group string|nil ----@class vim.Position ----@field public row number ----@field public col number +---@class vim.Position 1-based index +---@field public row integer +---@field public col integer ---@class vim.Range ---@field public start vim.Position diff --git a/bundle/nvim-cmp/lua/cmp/utils/api.lua b/bundle/nvim-cmp/lua/cmp/utils/api.lua index d0534092b..f304a78e3 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/api.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/api.lua @@ -44,9 +44,10 @@ api.get_current_line = function() return vim.api.nvim_get_current_line() end +---@return { [1]: integer, [2]: integer } api.get_cursor = function() if api.is_cmdline_mode() then - return { vim.o.lines - (vim.api.nvim_get_option('cmdheight') or 1) + 1, vim.fn.getcmdpos() - 1 } + return { math.min(vim.o.lines, vim.o.lines - (vim.api.nvim_get_option('cmdheight') - 1)), vim.fn.getcmdpos() - 1 } end return vim.api.nvim_win_get_cursor(0) end @@ -54,7 +55,7 @@ end api.get_screen_cursor = function() if api.is_cmdline_mode() then local cursor = api.get_cursor() - return { cursor[1], cursor[2] + 1 } + return { cursor[1], vim.fn.strdisplaywidth(string.sub(vim.fn.getcmdline(), 1, cursor[2] + 1)) } end local cursor = api.get_cursor() local pos = vim.fn.screenpos(0, cursor[1], cursor[2] + 1) diff --git a/bundle/nvim-cmp/lua/cmp/utils/api_spec.lua b/bundle/nvim-cmp/lua/cmp/utils/api_spec.lua index 5363b485b..31bbee244 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/api_spec.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/api_spec.lua @@ -4,8 +4,8 @@ local feedkeys = require('cmp.utils.feedkeys') local api = require('cmp.utils.api') describe('api', function() + before_each(spec.before) describe('get_cursor', function() - before_each(spec.before) it('insert-mode', function() local cursor feedkeys.call(keymap.t('i\t1234567890'), 'nx', function() @@ -24,8 +24,26 @@ describe('api', function() end) end) + describe('get_screen_cursor', function() + it('insert-mode', function() + local screen_cursor + feedkeys.call(keymap.t('iあいうえお'), 'nx', function() + screen_cursor = api.get_screen_cursor() + end) + assert.are.equal(10, screen_cursor[2]) + end) + it('cmdline-mode', function() + local screen_cursor + keymap.set_map(0, 'c', '(cmp-spec-spy)', function() + screen_cursor = api.get_screen_cursor() + end, { expr = true, noremap = true }) + feedkeys.call(keymap.t(':あいうえお'), 'n') + feedkeys.call(keymap.t('(cmp-spec-spy)'), 'x') + assert.are.equal(10, screen_cursor[2]) + end) + end) + describe('get_cursor_before_line', function() - before_each(spec.before) it('insert-mode', function() local cursor_before_line feedkeys.call(keymap.t('i\t1234567890'), 'nx', function() diff --git a/bundle/nvim-cmp/lua/cmp/utils/async.lua b/bundle/nvim-cmp/lua/cmp/utils/async.lua index 8c698e735..c62b565ee 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/async.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/async.lua @@ -1,18 +1,37 @@ +local feedkeys = require('cmp.utils.feedkeys') +local config = require('cmp.config') + local async = {} ---@class cmp.AsyncThrottle ---@field public running boolean ----@field public timeout number ----@field public sync function(self: cmp.AsyncThrottle, timeout: number|nil) +---@field public timeout integer +---@field public sync function(self: cmp.AsyncThrottle, timeout: integer|nil) ---@field public stop function ---@field public __call function +---@type uv_timer_t[] +local timers = {} + +vim.api.nvim_create_autocmd('VimLeavePre', { + callback = function() + for _, timer in pairs(timers) do + if timer and not timer:is_closing() then + timer:stop() + timer:close() + end + end + end, +}) + ---@param fn function ----@param timeout number +---@param timeout integer ---@return cmp.AsyncThrottle async.throttle = function(fn, timeout) local time = nil - local timer = vim.loop.new_timer() + local timer = assert(vim.loop.new_timer()) + local _async = nil ---@type Async? + timers[#timers + 1] = timer return setmetatable({ running = false, timeout = timeout, @@ -21,9 +40,15 @@ async.throttle = function(fn, timeout) return not self.running end) end, - stop = function() - time = nil + stop = function(reset_time) + if reset_time ~= false then + time = nil + end timer:stop() + if _async then + _async:cancel() + _async = nil + end end, }, { __call = function(self, ...) @@ -34,12 +59,23 @@ async.throttle = function(fn, timeout) end self.running = true - timer:stop() + self.stop(false) timer:start(math.max(1, self.timeout - (vim.loop.now() - time)), 0, function() vim.schedule(function() time = nil - fn(unpack(args)) - self.running = false + local ret = fn(unpack(args)) + if async.is_async(ret) then + ---@cast ret Async + _async = ret + _async:await(function(_, error) + self.running = false + if error and error ~= 'abort' then + vim.notify(error, vim.log.levels.ERROR) + end + end) + else + self.running = false + end end) end) end, @@ -60,7 +96,7 @@ end ---Timeout callback function ---@param fn function ----@param timeout number +---@param timeout integer ---@return function async.timeout = function(fn, timeout) local timer @@ -109,4 +145,146 @@ async.sync = function(runner, timeout) end, 10, false) end +---Wait and callback for next safe state. +async.debounce_next_tick = function(callback) + local running = false + return function() + if running then + return + end + running = true + vim.schedule(function() + running = false + callback() + end) + end +end + +---Wait and callback for consuming next keymap. +async.debounce_next_tick_by_keymap = function(callback) + return function() + feedkeys.call('', '', callback) + end +end + +local Scheduler = {} +Scheduler._queue = {} +Scheduler._executor = assert(vim.loop.new_check()) + +function Scheduler.step() + local budget = config.get().performance.async_budget * 1e6 + local start = vim.loop.hrtime() + while #Scheduler._queue > 0 and vim.loop.hrtime() - start < budget do + local a = table.remove(Scheduler._queue, 1) + a:_step() + if a.running then + table.insert(Scheduler._queue, a) + end + end + if #Scheduler._queue == 0 then + return Scheduler._executor:stop() + end +end + +---@param a Async +function Scheduler.add(a) + table.insert(Scheduler._queue, a) + if not Scheduler._executor:is_active() then + Scheduler._executor:start(vim.schedule_wrap(Scheduler.step)) + end +end + +--- @alias AsyncCallback fun(result?:any, error?:string) + +--- @class Async +--- @field running boolean +--- @field result? any +--- @field error? string +--- @field callbacks AsyncCallback[] +--- @field thread thread +local Async = {} +Async.__index = Async + +function Async.new(fn) + local self = setmetatable({}, Async) + self.callbacks = {} + self.running = true + self.thread = coroutine.create(fn) + Scheduler.add(self) + return self +end + +---@param result? any +---@param error? string +function Async:_done(result, error) + self.running = false + self.result = result + self.error = error + for _, callback in ipairs(self.callbacks) do + callback(result, error) + end +end + +function Async:_step() + local ok, res = coroutine.resume(self.thread) + if not ok then + return self:_done(nil, res) + elseif res == 'abort' then + return self:_done(nil, 'abort') + elseif coroutine.status(self.thread) == 'dead' then + return self:_done(res) + end +end + +function Async:cancel() + self.running = false +end + +---@param cb AsyncCallback +function Async:await(cb) + if not cb then + error('callback is required') + end + if self.running then + table.insert(self.callbacks, cb) + else + cb(self.result, self.error) + end +end + +function Async:sync() + while self.running do + vim.wait(10) + end + return self.error and error(self.error) or self.result +end + +--- @return boolean +function async.is_async(obj) + return obj and type(obj) == 'table' and getmetatable(obj) == Async +end + +--- @return fun(...): Async +function async.wrap(fn) + return function(...) + local args = { ... } + return Async.new(function() + return fn(unpack(args)) + end) + end +end + +-- This will yield when called from a coroutine +function async.yield(...) + if not coroutine.isyieldable() then + error('Trying to yield from a non-yieldable context') + return ... + end + return coroutine.yield(...) +end + +function async.abort() + return async.yield('abort') +end + return async diff --git a/bundle/nvim-cmp/lua/cmp/utils/autocmd.lua b/bundle/nvim-cmp/lua/cmp/utils/autocmd.lua index 4af766a07..438e23190 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/autocmd.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/autocmd.lua @@ -2,20 +2,38 @@ local debug = require('cmp.utils.debug') local autocmd = {} +autocmd.group = vim.api.nvim_create_augroup('___cmp___', { clear = true }) + autocmd.events = {} ---Subscribe autocmd ----@param event string +---@param events string|string[] ---@param callback function ---@return function -autocmd.subscribe = function(event, callback) - autocmd.events[event] = autocmd.events[event] or {} - table.insert(autocmd.events[event], callback) +autocmd.subscribe = function(events, callback) + events = type(events) == 'string' and { events } or events + + for _, event in ipairs(events) do + if not autocmd.events[event] then + autocmd.events[event] = {} + vim.api.nvim_create_autocmd(event, { + desc = ('nvim-cmp: autocmd: %s'):format(event), + group = autocmd.group, + callback = function() + autocmd.emit(event) + end, + }) + end + table.insert(autocmd.events[event], callback) + end + return function() - for i, callback_ in ipairs(autocmd.events[event]) do - if callback_ == callback then - table.remove(autocmd.events[event], i) - break + for _, event in ipairs(events) do + for i, callback_ in ipairs(autocmd.events[event]) do + if callback_ == callback then + table.remove(autocmd.events[event], i) + break + end end end end diff --git a/bundle/nvim-cmp/lua/cmp/utils/binary.lua b/bundle/nvim-cmp/lua/cmp/utils/binary.lua index ab1b0d225..b799f969e 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/binary.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/binary.lua @@ -3,7 +3,7 @@ local binary = {} ---Insert item to list to ordered index ---@param list any[] ---@param item any ----@param func fun(a: any, b: any): "1"|"-1"|"0" +---@param func fun(a: any, b: any): 1|-1|0 binary.insort = function(list, item, func) table.insert(list, binary.search(list, item, func), item) end @@ -11,8 +11,8 @@ end ---Search suitable index from list ---@param list any[] ---@param item any ----@param func fun(a: any, b: any): "1"|"-1"|"0" ----@return number +---@param func fun(a: any, b: any): 1|-1|0 +---@return integer binary.search = function(list, item, func) local s = 1 local e = #list diff --git a/bundle/nvim-cmp/lua/cmp/utils/buffer.lua b/bundle/nvim-cmp/lua/cmp/utils/buffer.lua index b48a1b1ff..0705cbc74 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/buffer.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/buffer.lua @@ -2,7 +2,7 @@ local buffer = {} buffer.cache = {} ----@return number buf +---@return integer buf buffer.get = function(name) local buf = buffer.cache[name] if buf and vim.api.nvim_buf_is_valid(buf) then @@ -12,7 +12,7 @@ buffer.get = function(name) end end ----@return number buf +---@return integer buf ---@return boolean created_new buffer.ensure = function(name) local created_new = false @@ -20,8 +20,6 @@ buffer.ensure = function(name) if not buf then created_new = true buf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_option(buf, 'buftype', 'nofile') - vim.api.nvim_buf_set_option(buf, 'bufhidden', 'hide') buffer.cache[name] = buf end return buf, created_new diff --git a/bundle/nvim-cmp/lua/cmp/utils/cache.lua b/bundle/nvim-cmp/lua/cmp/utils/cache.lua index 8607b2a3f..26456ad4b 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/cache.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/cache.lua @@ -9,7 +9,7 @@ cache.new = function() end ---Get cache value ----@param key string +---@param key string|string[] ---@return any|nil cache.get = function(self, key) key = self:key(key) @@ -20,7 +20,7 @@ cache.get = function(self, key) end ---Set cache value explicitly ----@param key string +---@param key string|string[] ---@vararg any cache.set = function(self, key, value) key = self:key(key) @@ -28,8 +28,10 @@ cache.set = function(self, key, value) end ---Ensure value by callback ----@param key string ----@param callback fun(): any +---@generic T +---@param key string|string[] +---@param callback fun(): T +---@return T cache.ensure = function(self, key, callback) local value = self:get(key) if value == nil then @@ -46,7 +48,7 @@ cache.clear = function(self) end ---Create key ----@param key string|table +---@param key string|string[] ---@return string cache.key = function(_, key) if type(key) == 'table' then diff --git a/bundle/nvim-cmp/lua/cmp/utils/char.lua b/bundle/nvim-cmp/lua/cmp/utils/char.lua index 12a1809e5..c733bef31 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/char.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/char.lua @@ -1,69 +1,71 @@ +local _ + local alpha = {} -string.gsub('abcdefghijklmnopqrstuvwxyz', '.', function(char) +_ = string.gsub('abcdefghijklmnopqrstuvwxyz', '.', function(char) alpha[string.byte(char)] = true end) local ALPHA = {} -string.gsub('ABCDEFGHIJKLMNOPQRSTUVWXYZ', '.', function(char) +_ = string.gsub('ABCDEFGHIJKLMNOPQRSTUVWXYZ', '.', function(char) ALPHA[string.byte(char)] = true end) local digit = {} -string.gsub('1234567890', '.', function(char) +_ = string.gsub('1234567890', '.', function(char) digit[string.byte(char)] = true end) local white = {} -string.gsub(' \t\n', '.', function(char) +_ = string.gsub(' \t\n', '.', function(char) white[string.byte(char)] = true end) local char = {} ----@param byte number +---@param byte integer ---@return boolean char.is_upper = function(byte) return ALPHA[byte] end ----@param byte number +---@param byte integer ---@return boolean char.is_alpha = function(byte) return alpha[byte] or ALPHA[byte] end ----@param byte number +---@param byte integer ---@return boolean char.is_digit = function(byte) return digit[byte] end ----@param byte number +---@param byte integer ---@return boolean char.is_white = function(byte) return white[byte] end ----@param byte number +---@param byte integer ---@return boolean char.is_symbol = function(byte) return not (char.is_alnum(byte) or char.is_white(byte)) end ----@param byte number +---@param byte integer ---@return boolean char.is_printable = function(byte) return string.match(string.char(byte), '^%c$') == nil end ----@param byte number +---@param byte integer ---@return boolean char.is_alnum = function(byte) return char.is_alpha(byte) or char.is_digit(byte) end ---@param text string ----@param index number +---@param index integer ---@return boolean char.is_semantic_index = function(text, index) if index <= 1 then @@ -89,8 +91,8 @@ char.is_semantic_index = function(text, index) end ---@param text string ----@param current_index number ----@return boolean +---@param current_index integer +---@return integer char.get_next_semantic_index = function(text, current_index) for i = current_index + 1, #text do if char.is_semantic_index(text, i) then @@ -101,8 +103,8 @@ char.get_next_semantic_index = function(text, current_index) end ---Ignore case match ----@param byte1 number ----@param byte2 number +---@param byte1 integer +---@param byte2 integer ---@return boolean char.match = function(byte1, byte2) if not char.is_alpha(byte1) or not char.is_alpha(byte2) then diff --git a/bundle/nvim-cmp/lua/cmp/utils/feedkeys.lua b/bundle/nvim-cmp/lua/cmp/utils/feedkeys.lua index e7810a7e5..cd20f6068 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/feedkeys.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/feedkeys.lua @@ -7,22 +7,18 @@ feedkeys.call = setmetatable({ callbacks = {}, }, { __call = function(self, keys, mode, callback) - if vim.fn.reg_recording() ~= '' then - return feedkeys.call_macro(keys, mode, callback) - end - local is_insert = string.match(mode, 'i') ~= nil local is_immediate = string.match(mode, 'x') ~= nil local queue = {} if #keys > 0 then - table.insert(queue, { keymap.t('set lazyredraw'), 'n' }) - table.insert(queue, { keymap.t('set textwidth=0'), 'n' }) - table.insert(queue, { keymap.t('set eventignore=all'), 'n' }) + table.insert(queue, { keymap.t('setlocal lazyredraw'), 'n' }) + table.insert(queue, { keymap.t('setlocal textwidth=0'), 'n' }) + table.insert(queue, { keymap.t('setlocal backspace=2'), 'n' }) table.insert(queue, { keys, string.gsub(mode, '[itx]', ''), true }) - table.insert(queue, { keymap.t('set %slazyredraw'):format(vim.o.lazyredraw and '' or 'no'), 'n' }) - table.insert(queue, { keymap.t('set textwidth=%s'):format(vim.bo.textwidth or 0), 'n' }) - table.insert(queue, { keymap.t('set eventignore=%s'):format(vim.o.eventignore or ''), 'n' }) + table.insert(queue, { keymap.t('setlocal %slazyredraw'):format(vim.o.lazyredraw and '' or 'no'), 'n' }) + table.insert(queue, { keymap.t('setlocal textwidth=%s'):format(vim.bo.textwidth or 0), 'n' }) + table.insert(queue, { keymap.t('setlocal backspace=%s'):format(vim.go.backspace or 2), 'n' }) end if callback then @@ -54,57 +50,4 @@ misc.set(_G, { 'cmp', 'utils', 'feedkeys', 'call', 'run' }, function(id) return '' end) -feedkeys.call_macro = setmetatable({ - queue = {}, - current = nil, - timer = vim.loop.new_timer(), - running = false, -}, { - __call = function(self, keys, mode, callback) - local is_insert = string.match(mode, 'i') ~= nil - table.insert(self.queue, is_insert and 1 or #self.queue + 1, { - keys = keys, - mode = mode, - callback = callback, - }) - - if not self.running then - self.running = true - local consume - consume = vim.schedule_wrap(function() - if vim.fn.getchar(1) == 0 then - if self.current then - vim.cmd(('set backspace=%s'):format(self.current.backspace or '')) - vim.cmd(('set eventignore=%s'):format(self.current.eventignore or '')) - if self.current.callback then - self.current.callback() - end - self.current = nil - end - - local current = table.remove(self.queue, 1) - if current then - self.current = { - keys = current.keys, - callback = current.callback, - backspace = vim.o.backspace, - eventignore = vim.o.eventignore, - } - vim.api.nvim_feedkeys(keymap.t('set backspace=start'), 'n', true) - vim.api.nvim_feedkeys(keymap.t('set eventignore=all'), '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 diff --git a/bundle/nvim-cmp/lua/cmp/utils/feedkeys_spec.lua b/bundle/nvim-cmp/lua/cmp/utils/feedkeys_spec.lua index a4e71f364..24fba71a4 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/feedkeys_spec.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/feedkeys_spec.lua @@ -23,6 +23,15 @@ describe('feedkeys', function() }) end) + it('bacckspace', function() + vim.cmd([[setlocal backspace=0]]) + feedkeys.call(keymap.t('iaiueo'), 'nx') + feedkeys.call(keymap.t('a'), 'nx') + assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), { + 'aiu', + }) + end) + it('testability', function() feedkeys.call('i', 'n', function() feedkeys.call('', 'n', function() diff --git a/bundle/nvim-cmp/lua/cmp/utils/highlight.lua b/bundle/nvim-cmp/lua/cmp/utils/highlight.lua index cbe25f5ce..867632a0c 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/highlight.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/highlight.lua @@ -1,46 +1,31 @@ local highlight = {} highlight.keys = { - 'gui', - 'guifg', - 'guibg', - 'cterm', - 'ctermfg', - 'ctermbg', + 'fg', + 'bg', + 'bold', + 'italic', + 'reverse', + 'standout', + 'underline', + 'undercurl', + 'strikethrough', } -highlight.inherit = function(name, source, override) - local cmd = ('highlight default %s'):format(name) +highlight.inherit = function(name, source, settings) for _, key in ipairs(highlight.keys) do - if override[key] then - cmd = cmd .. (' %s=%s'):format(key, override[key]) - else - local v = highlight.get(source, key) - v = v == '' and 'NONE' or v - cmd = cmd .. (' %s=%s'):format(key, v) - end - end - vim.cmd(cmd) -end - -highlight.get = function(source, key) - if key == 'gui' or key == 'cterm' then - local ui = {} - for _, k in ipairs({ 'bold', 'italic', 'reverse', 'inverse', 'standout', 'underline', 'undercurl', 'strikethrough' }) do - if vim.fn.synIDattr(vim.fn.hlID(source), k, key) == 1 then - table.insert(ui, k) + if not settings[key] then + local v = vim.fn.synIDattr(vim.fn.hlID(source), key) + if key == 'fg' or key == 'bg' then + local n = tonumber(v, 10) + v = type(n) == 'number' and n or v + else + v = v == 1 end + settings[key] = v == '' and 'NONE' or v end - return table.concat(ui, ',') - elseif key == 'guifg' then - return vim.fn.synIDattr(vim.fn.hlID(source), 'fg#', 'gui') - elseif key == 'guibg' then - return vim.fn.synIDattr(vim.fn.hlID(source), 'bg#', 'gui') - elseif key == 'ctermfg' then - return vim.fn.synIDattr(vim.fn.hlID(source), 'fg', 'term') - elseif key == 'ctermbg' then - return vim.fn.synIDattr(vim.fn.hlID(source), 'bg', 'term') end + vim.api.nvim_set_hl(0, name, settings) end return highlight diff --git a/bundle/nvim-cmp/lua/cmp/utils/keymap.lua b/bundle/nvim-cmp/lua/cmp/utils/keymap.lua index 5727cd096..5ab0be78e 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/keymap.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/keymap.lua @@ -1,4 +1,5 @@ local misc = require('cmp.utils.misc') +local buffer = require('cmp.utils.buffer') local api = require('cmp.utils.api') local keymap = {} @@ -16,12 +17,16 @@ end ---@param keys string ---@return string keymap.normalize = function(keys) - vim.api.nvim_set_keymap('t', '(cmp.utils.keymap.normalize)', keys, {}) - for _, map in ipairs(vim.api.nvim_get_keymap('t')) do - if keymap.equals(map.lhs, '(cmp.utils.keymap.normalize)') then - return map.rhs + local normalize_buf = buffer.ensure('cmp.util.keymap.normalize') + vim.api.nvim_buf_set_keymap(normalize_buf, 't', keys, '(cmp.utils.keymap.normalize)', {}) + for _, map in ipairs(vim.api.nvim_buf_get_keymap(normalize_buf, 't')) do + if keymap.t(map.rhs) == keymap.t('(cmp.utils.keymap.normalize)') then + vim.api.nvim_buf_del_keymap(normalize_buf, 't', keys) + return map.lhs end end + vim.api.nvim_buf_del_keymap(normalize_buf, 't', keys) + vim.api.nvim_buf_delete(normalize_buf, {}) return keys end @@ -64,7 +69,7 @@ keymap.undojoin = function() end ---Create backspace keys. ----@param count number +---@param count string|integer ---@return string keymap.backspace = function(count) if type(count) == 'string' then @@ -78,8 +83,23 @@ keymap.backspace = function(count) return table.concat(keys, '') end +---Create delete keys. +---@param count string|integer +---@return string +keymap.delete = function(count) + if type(count) == 'string' then + count = vim.fn.strchars(count, true) + end + if count <= 0 then + return '' + end + local keys = {} + table.insert(keys, keymap.t(string.rep('', count))) + return table.concat(keys, '') +end + ---Update indentkeys. ----@param expr string +---@param expr? string ---@return string keymap.indentkeys = function(expr) return string.format(keymap.t('set indentkeys=%s'), expr and vim.fn.escape(expr, '| \t\\') or '') @@ -90,7 +110,7 @@ end ---@param b string ---@return boolean keymap.equals = function(a, b) - return keymap.t(a) == keymap.t(b) + return keymap.normalize(a) == keymap.normalize(b) end ---Register keypress handler. @@ -98,8 +118,7 @@ keymap.listen = function(mode, lhs, callback) lhs = keymap.normalize(keymap.to_keymap(lhs)) local existing = keymap.get_map(mode, lhs) - local id = string.match(existing.rhs, 'v:lua%.cmp%.utils%.keymap%.set_map%((%d+)%)') - if id and keymap.set_map.callbacks[tonumber(id, 10)] then + if existing.desc == 'cmp.utils.keymap.set_map' then return end @@ -124,8 +143,8 @@ end keymap.fallback = function(bufnr, mode, map) return function() if map.expr then - local fallback_expr = string.format('(cmp.u.k.fallback_expr:%s)', map.lhs) - keymap.set_map(bufnr, mode, fallback_expr, function() + local fallback_lhs = string.format('(cmp.u.k.fallback_expr:%s)', map.lhs) + keymap.set_map(bufnr, mode, fallback_lhs, function() return keymap.solve(bufnr, mode, map).keys end, { expr = true, @@ -133,13 +152,14 @@ keymap.fallback = function(bufnr, mode, map) script = map.script, nowait = map.nowait, silent = map.silent and mode ~= 'c', + replace_keycodes = map.replace_keycodes, }) - vim.api.nvim_feedkeys(keymap.t(fallback_expr), 'im', true) - elseif not map.callback then + vim.api.nvim_feedkeys(keymap.t(fallback_lhs), 'im', true) + elseif map.callback then + map.callback() + else local solved = keymap.solve(bufnr, mode, map) vim.api.nvim_feedkeys(solved.keys, solved.mode, true) - else - map.callback() end end end @@ -147,7 +167,14 @@ end ---Solve keymap.solve = function(bufnr, mode, map) local lhs = keymap.t(map.lhs) - local rhs = map.expr and (map.callback and map.callback() or vim.api.nvim_eval(keymap.t(map.rhs))) or keymap.t(map.rhs) + local rhs = keymap.t(map.rhs) + if map.expr then + if map.callback then + rhs = map.callback() + else + rhs = vim.api.nvim_eval(keymap.t(map.rhs)) + end + end if map.noremap then return { keys = rhs, mode = 'in' } @@ -157,9 +184,10 @@ keymap.solve = function(bufnr, mode, map) local recursive = string.format('0_(cmp.u.k.recursive:%s)', lhs) keymap.set_map(bufnr, mode, recursive, lhs, { noremap = true, - script = map.script, + script = true, nowait = map.nowait, silent = map.silent and mode ~= 'c', + replace_keycodes = map.replace_keycodes, }) return { keys = keymap.t(recursive) .. string.gsub(rhs, '^' .. vim.pesc(lhs), ''), mode = 'im' } end @@ -180,11 +208,13 @@ keymap.get_map = function(mode, lhs) rhs = map.rhs or '', expr = map.expr == 1, callback = map.callback, + desc = map.desc, noremap = map.noremap == 1, script = map.script == 1, silent = map.silent == 1, nowait = map.nowait == 1, buffer = true, + replace_keycodes = map.replace_keycodes == 1, } end end @@ -196,11 +226,13 @@ keymap.get_map = function(mode, lhs) rhs = map.rhs or '', expr = map.expr == 1, callback = map.callback, + desc = map.desc, noremap = map.noremap == 1, script = map.script == 1, silent = map.silent == 1, nowait = map.nowait == 1, buffer = false, + replace_keycodes = map.replace_keycodes == 1, } end end @@ -215,33 +247,27 @@ keymap.get_map = function(mode, lhs) silent = true, nowait = false, buffer = false, + replace_keycodes = true, } end ---Set keymapping -keymap.set_map = setmetatable({ - callbacks = {}, -}, { - __call = function(self, bufnr, mode, lhs, rhs, opts) - if type(rhs) == 'function' then - local id = misc.id('cmp.utils.keymap.set_map') - self.callbacks[id] = rhs - if opts.expr then - rhs = ('v:lua.cmp.utils.keymap.set_map(%s)'):format(id) - else - rhs = ('call v:lua.cmp.utils.keymap.set_map(%s)'):format(id) - end - end +keymap.set_map = function(bufnr, mode, lhs, rhs, opts) + if type(rhs) == 'function' then + opts.callback = rhs + rhs = '' + end + opts.desc = 'cmp.utils.keymap.set_map' - if bufnr == -1 then - vim.api.nvim_set_keymap(mode, lhs, rhs, opts) - else - vim.api.nvim_buf_set_keymap(bufnr, mode, lhs, rhs, opts) - end - end, -}) -misc.set(_G, { 'cmp', 'utils', 'keymap', 'set_map' }, function(id) - return keymap.set_map.callbacks[id]() or '' -end) + if vim.fn.has('nvim-0.8') == 0 then + opts.replace_keycodes = nil + end + + if bufnr == -1 then + vim.api.nvim_set_keymap(mode, lhs, rhs, opts) + else + vim.api.nvim_buf_set_keymap(bufnr, mode, lhs, rhs, opts) + end +end return keymap diff --git a/bundle/nvim-cmp/lua/cmp/utils/misc.lua b/bundle/nvim-cmp/lua/cmp/utils/misc.lua index 8dd3529a8..fda6f4723 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/misc.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/misc.lua @@ -29,6 +29,24 @@ misc.concat = function(list1, list2) return new_list end +---Repeat values +---@generic T +---@param str_or_tbl T +---@param count integer +---@return T +misc.rep = function(str_or_tbl, count) + if type(str_or_tbl) == 'string' then + return string.rep(str_or_tbl, count) + end + local rep = {} + for _ = 1, count do + for _, v in ipairs(str_or_tbl) do + table.insert(rep, v) + end + end + return rep +end + ---Return the valu is empty or not. ---@param v any ---@return boolean @@ -56,42 +74,38 @@ misc.none = vim.NIL ---Merge two tables recursively ---@generic T ----@param v1 T ----@param v2 T +---@param tbl1 T +---@param tbl2 T ---@return T -misc.merge = function(v1, v2) - local merge1 = type(v1) == 'table' and (not vim.tbl_islist(v1) or vim.tbl_isempty(v1)) - local merge2 = type(v2) == 'table' and (not vim.tbl_islist(v2) or vim.tbl_isempty(v2)) - if merge1 and merge2 then +misc.merge = function(tbl1, tbl2) + local is_dict1 = type(tbl1) == 'table' and (not vim.tbl_islist(tbl1) or vim.tbl_isempty(tbl1)) + local is_dict2 = type(tbl2) == 'table' and (not vim.tbl_islist(tbl2) or vim.tbl_isempty(tbl2)) + if is_dict1 and is_dict2 then local new_tbl = {} - for k, v in pairs(v2) do - new_tbl[k] = misc.merge(v1[k], v) + for k, v in pairs(tbl2) do + if tbl1[k] ~= misc.none then + new_tbl[k] = misc.merge(tbl1[k], v) + end end - for k, v in pairs(v1) do - if v2[k] == nil and v ~= misc.none then - new_tbl[k] = v + for k, v in pairs(tbl1) do + if tbl2[k] == nil then + if v ~= misc.none then + new_tbl[k] = misc.merge(v, {}) + else + new_tbl[k] = nil + end end end return new_tbl end - if v1 == misc.none then - return nil - end - if v1 == nil then - if v2 == misc.none then - return nil - else - return v2 - end - end - if v1 == true then - if merge2 then - return v2 - end - return {} - end - return v1 + if tbl1 == misc.none then + return nil + elseif tbl1 == nil then + return misc.merge(tbl2, {}) + else + return tbl1 + end end ---Generate id for group name @@ -105,22 +119,12 @@ misc.id = setmetatable({ end, }) ----Check the value is nil or not. ----@param v boolean ----@return boolean -misc.safe = function(v) - if v == nil or v == vim.NIL then - return nil - end - return v -end - ---Treat 1/0 as bool value ----@param v boolean|"1"|"0" +---@param v boolean|1|0 ---@param def boolean ---@return boolean misc.bool = function(v, def) - if misc.safe(v) == nil then + if v == nil then return def end return v == true or v == 1 @@ -134,7 +138,7 @@ misc.set = function(t, keys, v) local c = t for i = 1, #keys - 1 do local key = keys[i] - c[key] = misc.safe(c[key]) or {} + c[key] = c[key] or {} c = c[key] end c[keys[#keys]] = v @@ -166,8 +170,8 @@ end ---Safe version of vim.str_utfindex ---@param text string ----@param vimindex number|nil ----@return number +---@param vimindex integer|nil +---@return integer misc.to_utfindex = function(text, vimindex) vimindex = vimindex or #text + 1 return vim.str_utfindex(text, math.max(0, math.min(vimindex - 1, #text))) @@ -175,8 +179,8 @@ end ---Safe version of vim.str_byteindex ---@param text string ----@param utfindex number ----@return number +---@param utfindex integer +---@return integer misc.to_vimindex = function(text, utfindex) utfindex = utfindex or #text for i = utfindex, 1, -1 do @@ -206,12 +210,14 @@ end misc.redraw = setmetatable({ doing = false, force = false, - termcode = vim.api.nvim_replace_termcodes('', true, true, true), + -- We use `` to redraw the screen. (Previously, We use . it will remove the unmatches search history.) + incsearch_redraw_keys = ' ', }, { __call = function(self, force) + local termcode = vim.api.nvim_replace_termcodes(self.incsearch_redraw_keys, true, true, true) if vim.tbl_contains({ '/', '?' }, vim.fn.getcmdtype()) then if vim.o.incsearch then - return vim.api.nvim_feedkeys(self.termcode, 'in', true) + return vim.api.nvim_feedkeys(termcode, 'ni', true) end end diff --git a/bundle/nvim-cmp/lua/cmp/utils/misc_spec.lua b/bundle/nvim-cmp/lua/cmp/utils/misc_spec.lua index 4e705efa1..f6871553b 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/misc_spec.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/misc_spec.lua @@ -16,6 +16,18 @@ describe('misc', function() }) assert.are.equal(merged.a.b, 1) + merged = misc.merge({ + a = { + i = 1, + }, + }, { + a = { + c = 2, + }, + }) + assert.are.equal(merged.a.i, 1) + assert.are.equal(merged.a.c, 2) + merged = misc.merge({ a = false, }, { diff --git a/bundle/nvim-cmp/lua/cmp/utils/options.lua b/bundle/nvim-cmp/lua/cmp/utils/options.lua new file mode 100644 index 000000000..7314ad1f4 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/options.lua @@ -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 diff --git a/bundle/nvim-cmp/lua/cmp/utils/str.lua b/bundle/nvim-cmp/lua/cmp/utils/str.lua index 450c9916b..df5e643bd 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/str.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/str.lua @@ -1,5 +1,4 @@ local char = require('cmp.utils.char') -local pattern = require('cmp.utils.pattern') local str = {} @@ -73,23 +72,6 @@ str.remove_suffix = function(text, suffix) return string.sub(text, 1, -#suffix - 1) end ----strikethrough ----@param text string ----@return string -str.strikethrough = function(text) - local r = pattern.regex('.') - local buffer = '' - while text ~= '' do - local s, e = r:match_str(text) - if not s then - break - end - buffer = buffer .. string.sub(text, s, e) .. '̶' - text = string.sub(text, e + 1) - end - return buffer -end - ---trim ---@param text string ---@return string @@ -117,8 +99,8 @@ end ---get_word ---@param text string ----@param stop_char number ----@param min_length number +---@param stop_char integer +---@param min_length integer ---@return string str.get_word = function(text, stop_char, min_length) min_length = min_length or 0 diff --git a/bundle/nvim-cmp/lua/cmp/utils/str_spec.lua b/bundle/nvim-cmp/lua/cmp/utils/str_spec.lua index 541414f21..1a218559f 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/str_spec.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/str_spec.lua @@ -12,10 +12,6 @@ describe('utils.str', function() assert.are.equal(str.get_word('import { GetStaticProps$1 } from "next";', nil, 9), 'import { GetStaticProps') end) - it('strikethrough', function() - assert.are.equal(str.strikethrough('あいうえお'), 'あ̶い̶う̶え̶お̶') - end) - it('remove_suffix', function() assert.are.equal(str.remove_suffix('log()', '$0'), 'log()') assert.are.equal(str.remove_suffix('log()$0', '$0'), 'log()') diff --git a/bundle/nvim-cmp/lua/cmp/utils/window.lua b/bundle/nvim-cmp/lua/cmp/utils/window.lua index 8e2bf02b5..80fb9f59c 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/window.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/window.lua @@ -1,25 +1,26 @@ -local cache = require('cmp.utils.cache') local misc = require('cmp.utils.misc') +local opt = require('cmp.utils.options') local buffer = require('cmp.utils.buffer') local api = require('cmp.utils.api') +local config = require('cmp.config') ---@class cmp.WindowStyle ---@field public relative string ----@field public row number ----@field public col number ----@field public width number ----@field public height number ----@field public zindex number|nil +---@field public row integer +---@field public col integer +---@field public width integer|float +---@field public height integer|float +---@field public border string|string[]|nil +---@field public zindex integer|nil ---@class cmp.Window ---@field public name string ----@field public win number|nil ----@field public swin1 number|nil ----@field public swin2 number|nil +---@field public win integer|nil +---@field public thumb_win integer|nil +---@field public sbar_win integer|nil ---@field public style cmp.WindowStyle ---@field public opt table ---@field public buffer_opt table ----@field public cache cmp.Cache local window = {} ---new @@ -28,10 +29,9 @@ window.new = function() local self = setmetatable({}, { __index = window }) self.name = misc.id('cmp.utils.window.new') self.win = nil - self.swin1 = nil - self.swin2 = nil + self.sbar_win = nil + self.thumb_win = nil self.style = {} - self.cache = cache.new() self.opt = {} self.buffer_opt = {} return self @@ -52,7 +52,7 @@ window.option = function(self, key, value) self.opt[key] = value if self:visible() then - vim.api.nvim_win_set_option(self.win, key, value) + opt.win_set_option(self.win, key, value) end end @@ -72,30 +72,35 @@ window.buffer_option = function(self, key, value) self.buffer_opt[key] = value local existing_buf = buffer.get(self.name) if existing_buf then - vim.api.nvim_buf_set_option(existing_buf, key, value) + opt.buf_set_option(existing_buf, key, value) end end ---Set style. ---@param style cmp.WindowStyle window.set_style = function(self, style) - if vim.o.columns and vim.o.columns <= style.col + style.width then - style.width = vim.o.columns - style.col - 1 - end - if vim.o.lines and vim.o.lines <= style.row + style.height then - style.height = vim.o.lines - style.row - 1 - end self.style = style + local info = self:info() + + if vim.o.lines and vim.o.lines <= info.row + info.height + 1 then + self.style.height = vim.o.lines - info.row - info.border_info.vert - 1 + end + self.style.zindex = self.style.zindex or 1 + + --- GUI clients are allowed to return fractional bounds, but we need integer + --- bounds to open the window + self.style.width = math.ceil(self.style.width) + self.style.height = math.ceil(self.style.height) end ---Return buffer id. ----@return number +---@return integer window.get_buffer = function(self) local buf, created_new = buffer.ensure(self.name) if created_new then for k, v in pairs(self.buffer_opt) do - vim.api.nvim_buf_set_option(buf, k, v) + opt.buf_set_option(buf, k, v) end end return buf @@ -119,7 +124,7 @@ window.open = function(self, style) s.noautocmd = true self.win = vim.api.nvim_open_win(self:get_buffer(), false, s) for k, v in pairs(self.opt) do - vim.api.nvim_win_set_option(self.win, k, v) + opt.win_set_option(self.win, k, v) end end self:update() @@ -127,49 +132,57 @@ end ---Update window.update = function(self) - if self:has_scrollbar() then - local total = self:get_content_height() - local info = self:info() - local bar_height = math.ceil(info.height * (info.height / total)) - local bar_offset = math.min(info.height - bar_height, math.floor(info.height * (vim.fn.getwininfo(self.win)[1].topline / total))) - local style1 = {} - style1.relative = 'editor' - style1.style = 'minimal' - style1.width = 1 - style1.height = info.height - style1.row = info.row - style1.col = info.col + info.width - (info.has_scrollbar and 1 or 0) - style1.zindex = (self.style.zindex and (self.style.zindex + 1) or 1) - if self.swin1 and vim.api.nvim_win_is_valid(self.swin1) then - vim.api.nvim_win_set_config(self.swin1, style1) - else - style1.noautocmd = true - self.swin1 = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbuf1'), false, style1) - vim.api.nvim_win_set_option(self.swin1, 'winhighlight', 'EndOfBuffer:PmenuSbar,Normal:PmenuSbar,NormalNC:PmenuSbar,NormalFloat:PmenuSbar') + local info = self:info() + if info.scrollable then + -- Draw the background of the scrollbar + + if not info.border_info.visible then + local style = { + relative = 'editor', + style = 'minimal', + width = 1, + height = self.style.height, + row = info.row, + col = info.col + info.width - info.scrollbar_offset, -- info.col was already contained the scrollbar offset. + zindex = (self.style.zindex and (self.style.zindex + 1) or 1), + } + if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then + vim.api.nvim_win_set_config(self.sbar_win, style) + else + style.noautocmd = true + self.sbar_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbar_buf'), false, style) + opt.win_set_option(self.sbar_win, 'winhighlight', 'EndOfBuffer:PmenuSbar,NormalFloat:PmenuSbar') + end end - local style2 = {} - style2.relative = 'editor' - style2.style = 'minimal' - style2.width = 1 - style2.height = bar_height - style2.row = info.row + bar_offset - style2.col = info.col + info.width - (info.has_scrollbar and 1 or 0) - style2.zindex = (self.style.zindex and (self.style.zindex + 2) or 2) - if self.swin2 and vim.api.nvim_win_is_valid(self.swin2) then - vim.api.nvim_win_set_config(self.swin2, style2) + + -- Draw the scrollbar thumb + local thumb_height = math.floor(info.inner_height * (info.inner_height / self:get_content_height()) + 0.5) + local thumb_offset = math.floor(info.inner_height * (vim.fn.getwininfo(self.win)[1].topline / self:get_content_height())) + + local style = { + relative = 'editor', + style = 'minimal', + width = 1, + height = math.max(1, thumb_height), + row = info.row + thumb_offset + (info.border_info.visible and info.border_info.top or 0), + col = info.col + info.width - 1, -- info.col was already added scrollbar offset. + zindex = (self.style.zindex and (self.style.zindex + 2) or 2), + } + if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then + vim.api.nvim_win_set_config(self.thumb_win, style) else - style2.noautocmd = true - self.swin2 = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbuf2'), false, style2) - vim.api.nvim_win_set_option(self.swin2, 'winhighlight', 'EndOfBuffer:PmenuThumb,Normal:PmenuThumb,NormalNC:PmenuThumb,NormalFloat:PmenuThumb') + style.noautocmd = true + self.thumb_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'thumb_buf'), false, style) + opt.win_set_option(self.thumb_win, 'winhighlight', 'EndOfBuffer:PmenuThumb,NormalFloat:PmenuThumb') end else - if self.swin1 and vim.api.nvim_win_is_valid(self.swin1) then - vim.api.nvim_win_hide(self.swin1) - self.swin1 = nil + if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then + vim.api.nvim_win_hide(self.sbar_win) + self.sbar_win = nil end - if self.swin2 and vim.api.nvim_win_is_valid(self.swin2) then - vim.api.nvim_win_hide(self.swin2) - self.swin2 = nil + if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then + vim.api.nvim_win_hide(self.thumb_win) + self.thumb_win = nil end end @@ -188,13 +201,13 @@ window.close = function(self) vim.api.nvim_win_hide(self.win) self.win = nil end - if self.swin1 and vim.api.nvim_win_is_valid(self.swin1) then - vim.api.nvim_win_hide(self.swin1) - self.swin1 = nil + if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then + vim.api.nvim_win_hide(self.sbar_win) + self.sbar_win = nil end - if self.swin2 and vim.api.nvim_win_is_valid(self.swin2) then - vim.api.nvim_win_hide(self.swin2) - self.swin2 = nil + if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then + vim.api.nvim_win_hide(self.thumb_win) + self.thumb_win = nil end end end @@ -204,91 +217,102 @@ window.visible = function(self) return self.win and vim.api.nvim_win_is_valid(self.win) end ----Return the scrollbar will shown or not. -window.has_scrollbar = function(self) - return (self.style.height or 0) < self:get_content_height() -end - ---Return win info. window.info = function(self) - local border_width = self:get_border_width() - local has_scrollbar = self:has_scrollbar() - return { + local border_info = self:get_border_info() + local scrollbar = config.get().window.completion.scrollbar + local info = { row = self.style.row, col = self.style.col, - width = self.style.width + border_width + (has_scrollbar and 1 or 0), - height = self.style.height, - border_width = border_width, - has_scrollbar = has_scrollbar, + width = self.style.width + border_info.left + border_info.right, + height = self.style.height + border_info.top + border_info.bottom, + inner_width = self.style.width, + inner_height = self.style.height, + border_info = border_info, + scrollable = false, + scrollbar_offset = 0, } + + if self:get_content_height() > info.inner_height and scrollbar then + info.scrollable = true + if not border_info.visible then + info.scrollbar_offset = 1 + info.width = info.width + 1 + end + end + + return info end ----Get border width ----@return number -window.get_border_width = function(self) +---Return border information. +---@return { top: integer, left: integer, right: integer, bottom: integer, vert: integer, horiz: integer, visible: boolean } +window.get_border_info = function(self) local border = self.style.border - if type(border) == 'table' then - local new_border = {} - while #new_border < 8 do - for _, b in ipairs(border) do - table.insert(new_border, b) - end + if not border or border == 'none' then + return { + top = 0, + left = 0, + right = 0, + bottom = 0, + vert = 0, + horiz = 0, + visible = false, + } + end + if type(border) == 'string' then + if border == 'shadow' then + return { + top = 0, + left = 0, + right = 1, + bottom = 1, + vert = 1, + horiz = 1, + visible = false, + } end - border = new_border + return { + top = 1, + left = 1, + right = 1, + bottom = 1, + vert = 2, + horiz = 2, + visible = true, + } end - local w = 0 - if border then - if type(border) == 'string' then - if border == 'single' then - w = 2 - elseif border == 'solid' then - w = 2 - elseif border == 'double' then - w = 2 - elseif border == 'rounded' then - w = 2 - elseif border == 'shadow' then - w = 1 - end - elseif type(border) == 'table' then - local b4 = type(border[4]) == 'table' and border[4][1] or border[4] - if #b4 > 0 then - w = w + 1 - end - local b8 = type(border[8]) == 'table' and border[8][1] or border[8] - if #b8 > 0 then - w = w + 1 - end + local new_border = {} + while #new_border <= 8 do + for _, b in ipairs(border) do + table.insert(new_border, type(b) == 'string' and b or b[1]) end end - return w + local info = {} + info.top = new_border[2] == '' and 0 or 1 + info.right = new_border[4] == '' and 0 or 1 + info.bottom = new_border[6] == '' and 0 or 1 + info.left = new_border[8] == '' and 0 or 1 + info.vert = info.top + info.bottom + info.horiz = info.left + info.right + info.visible = not (vim.tbl_contains({ '', ' ' }, new_border[2]) and vim.tbl_contains({ '', ' ' }, new_border[4]) and vim.tbl_contains({ '', ' ' }, new_border[6]) and vim.tbl_contains({ '', ' ' }, new_border[8])) + return info end ---Get scroll height. ----@return number +---NOTE: The result of vim.fn.strdisplaywidth depends on the buffer it was called in (see comment in cmp.Entry.get_view). +---@return integer window.get_content_height = function(self) if not self:option('wrap') then return vim.api.nvim_buf_line_count(self:get_buffer()) end - - return self.cache:ensure({ - 'get_content_height', - self.style.width, - self:get_buffer(), - vim.api.nvim_buf_get_changedtick(self:get_buffer()), - }, function() - local height = 0 - local buf = self:get_buffer() - -- The result of vim.fn.strdisplaywidth depends on the buffer it was called - -- in (see comment in cmp.Entry.get_view). - vim.api.nvim_buf_call(buf, function() - for _, text in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do - height = height + math.ceil(math.max(1, vim.fn.strdisplaywidth(text)) / self.style.width) - end - end) - return height + local height = 0 + vim.api.nvim_buf_call(self:get_buffer(), function() + for _, text in ipairs(vim.api.nvim_buf_get_lines(self:get_buffer(), 0, -1, false)) do + height = height + math.max(1, math.ceil(vim.fn.strdisplaywidth(text) / self.style.width)) + end end) + return height end return window diff --git a/bundle/nvim-cmp/lua/cmp/view.lua b/bundle/nvim-cmp/lua/cmp/view.lua index 981378b9d..ef7914254 100644 --- a/bundle/nvim-cmp/lua/cmp/view.lua +++ b/bundle/nvim-cmp/lua/cmp/view.lua @@ -47,6 +47,7 @@ end ---Open menu ---@param ctx cmp.Context ---@param sources cmp.Source[] +---@return boolean did_open view.open = function(self, ctx, sources) local source_group_map = {} for _, s in ipairs(sources) do @@ -104,10 +105,15 @@ view.open = function(self, ctx, sources) end end end) + local max_item_count = config.get().performance.max_view_entries or 200 + entries = vim.list_slice(entries, 1, max_item_count) -- open if #entries > 0 then self:_get_entries_view():open(offset, entries) + self.event:emit('menu_opened', { + window = self:_get_entries_view(), + }) break end end @@ -116,6 +122,7 @@ view.open = function(self, ctx, sources) if #entries == 0 then self:close() end + return #entries > 0 end ---Close menu @@ -128,6 +135,9 @@ view.close = function(self) self:_get_entries_view():close() self.docs_view:close() self.ghost_text_view:hide() + self.event:emit('menu_closed', { + window = self:_get_entries_view(), + }) end ---Abort menu @@ -135,6 +145,9 @@ view.abort = function(self) self:_get_entries_view():abort() self.docs_view:close() self.ghost_text_view:hide() + self.event:emit('menu_closed', { + window = self:_get_entries_view(), + }) end ---Return the view is visible or not. @@ -144,7 +157,7 @@ view.visible = function(self) end ---Scroll documentation window if possible. ----@param delta number +---@param delta integer view.scroll_docs = function(self, delta) self.docs_view:scroll(delta) end diff --git a/bundle/nvim-cmp/lua/cmp/view/custom_entries_view.lua b/bundle/nvim-cmp/lua/cmp/view/custom_entries_view.lua index 00044801b..a5eca58b1 100644 --- a/bundle/nvim-cmp/lua/cmp/view/custom_entries_view.lua +++ b/bundle/nvim-cmp/lua/cmp/view/custom_entries_view.lua @@ -8,13 +8,11 @@ local keymap = require('cmp.utils.keymap') local misc = require('cmp.utils.misc') local api = require('cmp.utils.api') -local SIDE_PADDING = 1 - local DEFAULT_HEIGHT = 10 -- @see https://github.com/vim/vim/blob/master/src/popupmenu.c#L45 ---@class cmp.CustomEntriesView ---@field private entries_win cmp.Window ----@field private offset number +---@field private offset integer ---@field private active boolean ---@field private entries cmp.Entry[] ---@field private column_width any @@ -25,20 +23,21 @@ custom_entries_view.ns = vim.api.nvim_create_namespace('cmp.view.custom_entries_ custom_entries_view.new = function() local self = setmetatable({}, { __index = custom_entries_view }) + self.entries_win = window.new() self.entries_win:option('conceallevel', 2) self.entries_win:option('concealcursor', 'n') self.entries_win:option('cursorlineopt', 'line') self.entries_win:option('foldenable', false) self.entries_win:option('wrap', false) - self.entries_win:option('scrolloff', 0) - self.entries_win:option('winhighlight', 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None') -- This is done so that strdisplaywidth calculations for lines in the -- custom_entries_view window exactly match with what is really displayed, -- see comment in cmp.Entry.get_view. Setting tabstop to 1 makes all tabs be -- always rendered one column wide, which removes the unpredictability coming -- from variable width of the tab character. self.entries_win:buffer_option('tabstop', 1) + self.entries_win:buffer_option('filetype', 'cmp_menu') + self.entries_win:buffer_option('buftype', 'nofile') self.event = event.new() self.offset = -1 self.active = false @@ -65,7 +64,7 @@ custom_entries_view.new = function() local e = self.entries[i + 1] if e then local v = e:get_view(self.offset, buf) - local o = SIDE_PADDING + local o = config.get().window.completion.side_padding local a = 0 for _, field in ipairs(fields) do if field == types.cmp.ItemField.Abbr then @@ -118,17 +117,15 @@ custom_entries_view.is_direction_top_down = function(self) end custom_entries_view.open = function(self, offset, entries) + local completion = config.get().window.completion self.offset = offset self.entries = {} self.column_width = { abbr = 0, kind = 0, menu = 0 } - -- Apply window options (that might be changed) on the custom completion menu. - self.entries_win:option('winblend', vim.o.pumblend) - local entries_buf = self.entries_win:get_buffer() local lines = {} local dedup = {} - local preselect = 0 + local preselect_index = 0 for _, e in ipairs(entries) do local view = e:get_view(offset, entries_buf) if view.dup == 1 or not dedup[e.completion_item.label] then @@ -138,8 +135,8 @@ custom_entries_view.open = function(self, offset, entries) self.column_width.menu = math.max(self.column_width.menu, view.menu.width) table.insert(self.entries, e) table.insert(lines, ' ') - if preselect == 0 and e.completion_item.preselect then - preselect = #self.entries + if preselect_index == 0 and e.completion_item.preselect then + preselect_index = #self.entries end end end @@ -157,18 +154,26 @@ custom_entries_view.open = function(self, offset, entries) height = math.min(height, #self.entries) local pos = api.get_screen_cursor() - local cursor = api.get_cursor() - local delta = cursor[2] + 1 - self.offset - local has_bottom_space = (vim.o.lines - pos[1]) >= DEFAULT_HEIGHT + local cursor_before_line = api.get_cursor_before_line() + local delta = vim.fn.strdisplaywidth(cursor_before_line:sub(self.offset)) local row, col = pos[1], pos[2] - delta - 1 - if not has_bottom_space and math.floor(vim.o.lines * 0.5) <= row and vim.o.lines - row <= height then + local border_info = window.get_border_info({ style = completion }) + local border_offset_row = border_info.top + border_info.bottom + local border_offset_col = border_info.left + border_info.right + if math.floor(vim.o.lines * 0.5) <= row + border_offset_row and vim.o.lines - row - border_offset_row <= math.min(DEFAULT_HEIGHT, height) then height = math.min(height, row - 1) - row = row - height - 1 + row = row - height - border_offset_row - 1 + if row < 0 then + height = height + row + end end - if math.floor(vim.o.columns * 0.5) <= col and vim.o.columns - col <= width then + if math.floor(vim.o.columns * 0.5) <= col + border_offset_col and vim.o.columns - col - border_offset_col <= width then width = math.min(width, vim.o.columns - 1) - col = vim.o.columns - width - 1 + col = vim.o.columns - width - border_offset_col - 1 + if col < 0 then + width = width + col + end end if pos[1] > row then @@ -182,35 +187,40 @@ custom_entries_view.open = function(self, offset, entries) for i = 1, math.floor(n / 2) do self.entries[i], self.entries[n - i + 1] = self.entries[n - i + 1], self.entries[i] end - if preselect ~= 0 then - preselect = #self.entries - preselect + 1 + if preselect_index ~= 0 then + preselect_index = #self.entries - preselect_index + 1 end end + -- Apply window options (that might be changed) on the custom completion menu. + self.entries_win:option('winblend', vim.o.pumblend) + self.entries_win:option('winhighlight', completion.winhighlight) + self.entries_win:option('scrolloff', completion.scrolloff) self.entries_win:open({ relative = 'editor', style = 'minimal', row = math.max(0, row), - col = math.max(0, col), + col = math.max(0, col + completion.col_offset), width = width, height = height, - zindex = 1001, + border = completion.border, + zindex = completion.zindex or 1001, }) -- always set cursor when starting. It will be adjusted on the call to _select vim.api.nvim_win_set_cursor(self.entries_win.win, { 1, 0 }) - if preselect > 0 and config.get().preselect == types.cmp.PreselectMode.Item then - self:_select(preselect, { behavior = types.cmp.SelectBehavior.Select }) + if preselect_index > 0 and config.get().preselect == types.cmp.PreselectMode.Item then + self:_select(preselect_index, { behavior = types.cmp.SelectBehavior.Select, active = false }) elseif not string.match(config.get().completion.completeopt, 'noselect') then if self:is_direction_top_down() then - self:_select(1, { behavior = types.cmp.SelectBehavior.Select }) + self:_select(1, { behavior = types.cmp.SelectBehavior.Select, active = false }) else - self:_select(#self.entries - 1, { behavior = types.cmp.SelectBehavior.Select }) + self:_select(#self.entries, { behavior = types.cmp.SelectBehavior.Select, active = false }) end else if self:is_direction_top_down() then - self:_select(0, { behavior = types.cmp.SelectBehavior.Select }) + self:_select(0, { behavior = types.cmp.SelectBehavior.Select, active = false }) else - self:_select(#self.entries + 1, { behavior = types.cmp.SelectBehavior.Select }) + self:_select(#self.entries + 1, { behavior = types.cmp.SelectBehavior.Select, active = false }) end end end @@ -245,12 +255,12 @@ custom_entries_view.draw = function(self) if e then local view = e:get_view(self.offset, entries_buf) local text = {} - table.insert(text, string.rep(' ', SIDE_PADDING)) + table.insert(text, string.rep(' ', config.get().window.completion.side_padding)) for _, field in ipairs(fields) do table.insert(text, view[field].text) table.insert(text, string.rep(' ', 1 + self.column_width[field] - view[field].width)) end - table.insert(text, string.rep(' ', SIDE_PADDING)) + table.insert(text, string.rep(' ', config.get().window.completion.side_padding)) table.insert(texts, table.concat(text, '')) end end @@ -275,34 +285,74 @@ end custom_entries_view.select_next_item = function(self, option) if self:visible() then local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1] - if self:is_direction_top_down() then - cursor = cursor + 1 - else - cursor = cursor - 1 - end + local is_top_down = self:is_direction_top_down() + local last = #self.entries + if not self.entries_win:option('cursorline') then - cursor = (self:is_direction_top_down() and 1) or #self.entries - elseif #self.entries < cursor then - cursor = (not self:is_direction_top_down() and #self.entries + 1) or 0 + cursor = (is_top_down and 1) or last + else + if is_top_down then + if cursor == last then + cursor = 0 + else + cursor = cursor + option.count + if last < cursor then + cursor = last + end + end + else + if cursor == 0 then + cursor = last + else + cursor = cursor - option.count + if cursor < 0 then + cursor = 0 + end + end + end end - self:_select(cursor, option) + + self:_select(cursor, { + behavior = option.behavior or types.cmp.SelectBehavior.Insert, + active = true, + }) end end custom_entries_view.select_prev_item = function(self, option) if self:visible() then local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1] - if self:is_direction_top_down() then - cursor = cursor - 1 - else - cursor = cursor + 1 - end + local is_top_down = self:is_direction_top_down() + local last = #self.entries + if not self.entries_win:option('cursorline') then - cursor = (self:is_direction_top_down() and #self.entries) or 1 - elseif #self.entries < cursor then - cursor = (not self:is_direction_top_down() and 0) or #self.entries + 1 + cursor = (is_top_down and last) or 1 + else + if is_top_down then + if cursor == 1 then + cursor = 0 + else + cursor = cursor - option.count + if cursor < 0 then + cursor = 1 + end + end + else + if cursor == last then + cursor = 0 + else + cursor = cursor + option.count + if last < cursor then + cursor = last + end + end + end end - self:_select(cursor, option) + + self:_select(cursor, { + behavior = option.behavior or types.cmp.SelectBehavior.Insert, + active = true, + }) end end @@ -343,10 +393,9 @@ custom_entries_view._select = function(self, cursor, option) if is_insert and not self.active then self.prefix = string.sub(api.get_current_line(), self.offset, api.get_cursor()[2]) or '' end + self.active = (0 < cursor and cursor <= #self.entries and option.active == true) - self.active = cursor > 0 and cursor <= #self.entries and is_insert self.entries_win:option('cursorline', cursor > 0 and cursor <= #self.entries) - vim.api.nvim_win_set_cursor(self.entries_win.win, { math.max(math.min(cursor, #self.entries), 1), 0, @@ -368,7 +417,17 @@ custom_entries_view._insert = setmetatable({ word = word or '' if api.is_cmdline_mode() then local cursor = api.get_cursor() - vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true) + -- setcmdline() added in v0.8.0 + if vim.fn.has('nvim-0.8') == 1 then + local current_line = api.get_current_line() + local before_line = current_line:sub(1, self.offset - 1) + local after_line = current_line:sub(cursor[2] + 1) + local pos = #before_line + #word + 1 + vim.fn.setcmdline(before_line .. word .. after_line, pos) + vim.api.nvim_feedkeys(keymap.t('redraw'), 'ni', false) + else + vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true) + end else if this.pending then return diff --git a/bundle/nvim-cmp/lua/cmp/view/docs_view.lua b/bundle/nvim-cmp/lua/cmp/view/docs_view.lua index 5a9acb4b7..3947e33f8 100644 --- a/bundle/nvim-cmp/lua/cmp/view/docs_view.lua +++ b/bundle/nvim-cmp/lua/cmp/view/docs_view.lua @@ -15,7 +15,10 @@ docs_view.new = function() self.window:option('foldenable', false) self.window:option('linebreak', true) self.window:option('scrolloff', 0) + self.window:option('showbreak', 'NONE') self.window:option('wrap', true) + self.window:buffer_option('filetype', 'cmp_docs') + self.window:buffer_option('buftype', 'nofile') return self end @@ -23,7 +26,7 @@ end ---@param e cmp.Entry ---@param view cmp.WindowStyle docs_view.open = function(self, e, view) - local documentation = config.get().documentation + local documentation = config.get().window.documentation if not documentation then return end @@ -32,11 +35,12 @@ docs_view.open = function(self, e, view) return self:close() end - local right_space = vim.o.columns - (view.col + view.width) - 2 - local left_space = view.col - 2 - local maxwidth = math.min(documentation.maxwidth, math.max(left_space, right_space) - 1) + local border_info = window.get_border_info({ style = documentation }) + local right_space = vim.o.columns - (view.col + view.width) - 1 + local left_space = view.col - 1 + local max_width = math.min(documentation.max_width, math.max(left_space, right_space)) - -- update buffer content if needed. + -- Update buffer content if needed. if not self.entry or e.id ~= self.entry.id then local documents = e:get_documentation() if #documents == 0 then @@ -46,24 +50,29 @@ docs_view.open = function(self, e, view) self.entry = e vim.api.nvim_buf_call(self.window:get_buffer(), function() vim.cmd([[syntax clear]]) + vim.api.nvim_buf_set_lines(self.window:get_buffer(), 0, -1, false, {}) end) vim.lsp.util.stylize_markdown(self.window:get_buffer(), documents, { - max_width = maxwidth, - max_height = documentation.maxheight, + max_width = max_width - border_info.horiz, + max_height = documentation.max_height, }) end + -- Set buffer as not modified, so it can be removed without errors + vim.api.nvim_buf_set_option(self.window:get_buffer(), 'modified', false) + + -- Calculate window size. local width, height = vim.lsp.util._make_floating_popup_size(vim.api.nvim_buf_get_lines(self.window:get_buffer(), 0, -1, false), { - max_width = maxwidth, - max_height = documentation.maxheight, + max_width = max_width - border_info.horiz, + max_height = documentation.max_height - border_info.vert, }) if width <= 0 or height <= 0 then return self:close() end + -- Calculate window position. local right_col = view.col + view.width - local left_col = view.col - width - 2 - + local left_col = view.col - width - border_info.horiz local col, left if right_space >= width and left_space >= width then if right_space < left_space then @@ -81,8 +90,10 @@ docs_view.open = function(self, e, view) return self:close() end + -- Render window. + self.window:option('winblend', vim.o.pumblend) self.window:option('winhighlight', documentation.winhighlight) - self.window:set_style({ + local style = { relative = 'editor', style = 'minimal', width = width, @@ -91,11 +102,14 @@ docs_view.open = function(self, e, view) col = col, border = documentation.border, zindex = documentation.zindex or 50, - }) - if left and self.window:has_scrollbar() then - self.window.style.col = self.window.style.col - 1 + } + self.window:open(style) + + -- Correct left-col for scrollbar existence. + if left then + style.col = style.col - self.window:info().scrollbar_offset + self.window:open(style) end - self.window:open() end ---Close floating window diff --git a/bundle/nvim-cmp/lua/cmp/view/ghost_text_view.lua b/bundle/nvim-cmp/lua/cmp/view/ghost_text_view.lua index b798ebb38..0b3be5cd6 100644 --- a/bundle/nvim-cmp/lua/cmp/view/ghost_text_view.lua +++ b/bundle/nvim-cmp/lua/cmp/view/ghost_text_view.lua @@ -9,6 +9,18 @@ local ghost_text_view = {} ghost_text_view.ns = vim.api.nvim_create_namespace('cmp:GHOST_TEXT') +local has_inline = (function() + return (pcall(function() + local id = vim.api.nvim_buf_set_extmark(0, ghost_text_view.ns, 0, 0, { + virt_text = { { ' ', 'Comment' } }, + virt_text_pos = 'inline', + hl_mode = 'combine', + ephemeral = true, + }) + vim.api.nvim_buf_del_extmark(0, ghost_text_view.ns, id) + end)) +end)() + ghost_text_view.new = function() local self = setmetatable({}, { __index = ghost_text_view }) self.win = nil @@ -17,7 +29,7 @@ ghost_text_view.new = function() on_win = function(_, win) return win == self.win end, - on_line = function(_) + on_line = function(_, _, _, on_row) local c = config.get().experimental.ghost_text if not c then return @@ -28,17 +40,23 @@ ghost_text_view.new = function() end local row, col = unpack(vim.api.nvim_win_get_cursor(0)) - local line = vim.api.nvim_get_current_line() - if string.sub(line, col + 1) ~= '' then + if on_row ~= row - 1 then return end + local line = vim.api.nvim_get_current_line() + if not has_inline then + if string.sub(line, col + 1) ~= '' then + return + end + end + local text = self.text_gen(self, line, col) if #text > 0 then vim.api.nvim_buf_set_extmark(0, ghost_text_view.ns, row - 1, col, { right_gravity = false, - virt_text = { { text, c.hl_group or 'Comment' } }, - virt_text_pos = 'overlay', + virt_text = { { text, type(c) == 'table' and c.hl_group or 'Comment' } }, + virt_text_pos = has_inline and 'inline' or 'overlay', hl_mode = 'combine', ephemeral = true, }) @@ -78,6 +96,10 @@ ghost_text_view.show = function(self, e) if not api.is_insert_mode() then return end + local c = config.get().experimental.ghost_text + if not c then + return + end local changed = e ~= self.entry self.win = vim.api.nvim_get_current_win() self.entry = e diff --git a/bundle/nvim-cmp/lua/cmp/view/native_entries_view.lua b/bundle/nvim-cmp/lua/cmp/view/native_entries_view.lua index d1dc8de7a..35ad4f2a5 100644 --- a/bundle/nvim-cmp/lua/cmp/view/native_entries_view.lua +++ b/bundle/nvim-cmp/lua/cmp/view/native_entries_view.lua @@ -7,10 +7,10 @@ local config = require('cmp.config') local api = require('cmp.utils.api') ---@class cmp.NativeEntriesView ----@field private offset number +---@field private offset integer ---@field private items vim.CompletedItem ---@field private entries cmp.Entry[] ----@field private preselect_index number +---@field private preselect_index integer ---@field public event cmp.Event local native_entries_view = {} @@ -77,8 +77,7 @@ native_entries_view.open = function(self, offset, entries) end native_entries_view.close = function(self) - if api.is_suitable_mode() and self:visible() then - vim.fn.complete(1, {}) + if api.is_insert_mode() and self:visible() then vim.api.nvim_select_popupmenu_item(-1, false, true, {}) end self.offset = -1 @@ -101,10 +100,10 @@ native_entries_view.info = function(self) if self:visible() then local info = vim.fn.pum_getpos() return { - width = info.width + (info.scrollbar and 1 or 0), + width = info.width + (info.scrollbar and 1 or 0) + (info.col == 0 and 0 or 1), height = info.height, row = info.row, - col = info.col, + col = info.col == 0 and 0 or info.col - 1, } end end @@ -123,9 +122,9 @@ native_entries_view.select_next_item = function(self, option) end if self:visible() then if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then - feedkeys.call(keymap.t(''), 'n', callback) + feedkeys.call(keymap.t(string.rep('', option.count)), 'n', callback) else - feedkeys.call(keymap.t(''), 'n', callback) + feedkeys.call(keymap.t(string.rep('', option.count)), 'n', callback) end end end @@ -136,9 +135,9 @@ native_entries_view.select_prev_item = function(self, option) end if self:visible() then if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then - feedkeys.call(keymap.t(''), 'n', callback) + feedkeys.call(keymap.t(string.rep('', option.count)), 'n', callback) else - feedkeys.call(keymap.t(''), 'n', callback) + feedkeys.call(keymap.t(string.rep('', option.count)), 'n', callback) end end end diff --git a/bundle/nvim-cmp/lua/cmp/view/wildmenu_entries_view.lua b/bundle/nvim-cmp/lua/cmp/view/wildmenu_entries_view.lua index 3419164f4..e5608d6d8 100644 --- a/bundle/nvim-cmp/lua/cmp/view/wildmenu_entries_view.lua +++ b/bundle/nvim-cmp/lua/cmp/view/wildmenu_entries_view.lua @@ -9,7 +9,7 @@ local misc = require('cmp.utils.misc') local api = require('cmp.utils.api') ---@class cmp.CustomEntriesView ----@field private offset number +---@field private offset integer ---@field private entries_win cmp.Window ---@field private active boolean ---@field private entries cmp.Entry[] @@ -181,11 +181,14 @@ end wildmenu_entries_view.select_next_item = function(self, option) if self:visible() then + local cursor if self.selected_index == 0 or self.selected_index == #self.entries then - self:_select(1, option) + cursor = option.count else - self:_select(self.selected_index + 1, option) + cursor = self.selected_index + option.count end + cursor = math.max(math.min(cursor, #self.entries), 0) + self:_select(cursor, option) end end @@ -194,7 +197,7 @@ wildmenu_entries_view.select_prev_item = function(self, option) if self.selected_index == 0 or self.selected_index <= 1 then self:_select(#self.entries, option) else - self:_select(self.selected_index - 1, option) + self:_select(math.max(self.selected_index - option.count, 0), option) end end end diff --git a/bundle/nvim-cmp/lua/cmp/vim_source.lua b/bundle/nvim-cmp/lua/cmp/vim_source.lua index 2ee8fbf37..5af9a72a3 100644 --- a/bundle/nvim-cmp/lua/cmp/vim_source.lua +++ b/bundle/nvim-cmp/lua/cmp/vim_source.lua @@ -2,7 +2,7 @@ local misc = require('cmp.utils.misc') local vim_source = {} ----@param id number +---@param id integer ---@param args any[] vim_source.on_callback = function(id, args) if vim_source.to_callback.callbacks[id] then @@ -11,7 +11,7 @@ vim_source.on_callback = function(id, args) end ---@param callback function ----@return number +---@return integer vim_source.to_callback = setmetatable({ callbacks = {}, }, { @@ -36,7 +36,7 @@ vim_source.to_args = function(args) return args end ----@param bridge_id number +---@param bridge_id integer ---@param methods string[] vim_source.new = function(bridge_id, methods) local self = {} diff --git a/bundle/nvim-cmp/nvim-cmp-scm-1.rockspec b/bundle/nvim-cmp/nvim-cmp-scm-1.rockspec new file mode 100644 index 000000000..3b6e93ea9 --- /dev/null +++ b/bundle/nvim-cmp/nvim-cmp-scm-1.rockspec @@ -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' + } +} diff --git a/bundle/nvim-cmp/plugin/cmp.lua b/bundle/nvim-cmp/plugin/cmp.lua index a977a78a7..eaacad83f 100644 --- a/bundle/nvim-cmp/plugin/cmp.lua +++ b/bundle/nvim-cmp/plugin/cmp.lua @@ -3,140 +3,57 @@ if vim.g.loaded_cmp then end vim.g.loaded_cmp = true -local api = require "cmp.utils.api" -local misc = require('cmp.utils.misc') +if not vim.api.nvim_create_autocmd then + return print('[nvim-cmp] Your nvim does not has `nvim_create_autocmd` function. Please update to latest nvim.') +end + +local api = require('cmp.utils.api') local types = require('cmp.types') -local config = require('cmp.config') local highlight = require('cmp.utils.highlight') +local autocmd = require('cmp.utils.autocmd') --- TODO: https://github.com/neovim/neovim/pull/14661 -vim.cmd [[ - augroup ___cmp___ - autocmd! - autocmd InsertEnter * lua require'cmp.utils.autocmd'.emit('InsertEnter') - autocmd InsertLeave * lua require'cmp.utils.autocmd'.emit('InsertLeave') - autocmd TextChangedI,TextChangedP * lua require'cmp.utils.autocmd'.emit('TextChanged') - autocmd CursorMovedI * lua require'cmp.utils.autocmd'.emit('CursorMoved') - autocmd CompleteChanged * lua require'cmp.utils.autocmd'.emit('CompleteChanged') - autocmd CompleteDone * lua require'cmp.utils.autocmd'.emit('CompleteDone') - autocmd ColorScheme * call v:lua.cmp.plugin.colorscheme() - autocmd CmdlineEnter * call v:lua.cmp.plugin.cmdline.enter() - autocmd CmdwinEnter * call v:lua.cmp.plugin.cmdline.leave() " for entering cmdwin with `` - augroup END -]] - -misc.set(_G, { 'cmp', 'plugin', 'cmdline', 'enter' }, function() - if config.is_native_menu() then - return +vim.api.nvim_set_hl(0, 'CmpItemAbbr', { link = 'CmpItemAbbrDefault', default = true }) +vim.api.nvim_set_hl(0, 'CmpItemAbbrDeprecated', { link = 'CmpItemAbbrDeprecatedDefault', default = true }) +vim.api.nvim_set_hl(0, 'CmpItemAbbrMatch', { link = 'CmpItemAbbrMatchDefault', default = true }) +vim.api.nvim_set_hl(0, 'CmpItemAbbrMatchFuzzy', { link = 'CmpItemAbbrMatchFuzzyDefault', default = true }) +vim.api.nvim_set_hl(0, 'CmpItemKind', { link = 'CmpItemKindDefault', default = true }) +vim.api.nvim_set_hl(0, 'CmpItemMenu', { link = 'CmpItemMenuDefault', default = true }) +for kind in pairs(types.lsp.CompletionItemKind) do + if type(kind) == 'string' then + local name = ('CmpItemKind%s'):format(kind) + vim.api.nvim_set_hl(0, name, { link = ('%sDefault'):format(name), default = true }) end - if vim.fn.expand('')~= '=' then - vim.schedule(function() - if api.is_cmdline_mode() then - vim.cmd [[ - augroup cmp-cmdline - autocmd! - autocmd CmdlineChanged * lua require'cmp.utils.autocmd'.emit('TextChanged') - autocmd CmdlineLeave * call v:lua.cmp.plugin.cmdline.leave() - augroup END - ]] - require('cmp.utils.autocmd').emit('CmdlineEnter') - end - end) - end -end) +end -misc.set(_G, { 'cmp', 'plugin', 'cmdline', 'leave' }, function() - if vim.fn.expand('') ~= '=' then - vim.cmd [[ - augroup cmp-cmdline - autocmd! - augroup END - ]] - require('cmp.utils.autocmd').emit('CmdlineLeave') - end -end) - -misc.set(_G, { 'cmp', 'plugin', 'colorscheme' }, function() - highlight.inherit('CmpItemAbbrDefault', 'Pmenu', { - guibg = 'NONE', - ctermbg = 'NONE', - }) - highlight.inherit('CmpItemAbbrDeprecatedDefault', 'Comment', { - gui = 'NONE', - guibg = 'NONE', - ctermbg = 'NONE', - }) - highlight.inherit('CmpItemAbbrMatchDefault', 'Pmenu', { - gui = 'NONE', - guibg = 'NONE', - ctermbg = 'NONE', - }) - highlight.inherit('CmpItemAbbrMatchFuzzyDefault', 'Pmenu', { - gui = 'NONE', - guibg = 'NONE', - ctermbg = 'NONE', - }) - highlight.inherit('CmpItemKindDefault', 'Special', { - guibg = 'NONE', - ctermbg = 'NONE', - }) +autocmd.subscribe('ColorScheme', function() + highlight.inherit('CmpItemAbbrDefault', 'Pmenu', { bg = 'NONE', default = false }) + highlight.inherit('CmpItemAbbrDeprecatedDefault', 'Comment', { bg = 'NONE', default = false }) + highlight.inherit('CmpItemAbbrMatchDefault', 'Pmenu', { bg = 'NONE', default = false }) + highlight.inherit('CmpItemAbbrMatchFuzzyDefault', 'Pmenu', { bg = 'NONE', default = false }) + highlight.inherit('CmpItemKindDefault', 'Special', { bg = 'NONE', default = false }) + highlight.inherit('CmpItemMenuDefault', 'Pmenu', { bg = 'NONE', default = false }) for name in pairs(types.lsp.CompletionItemKind) do if type(name) == 'string' then - vim.cmd(([[highlight default link CmpItemKind%sDefault CmpItemKind]]):format(name)) + vim.api.nvim_set_hl(0, ('CmpItemKind%sDefault'):format(name), { link = 'CmpItemKind', default = false }) end end - highlight.inherit('CmpItemMenuDefault', 'Pmenu', { - guibg = 'NONE', - ctermbg = 'NONE', - }) end) -_G.cmp.plugin.colorscheme() - -if vim.fn.hlexists('CmpItemAbbr') ~= 1 then - vim.cmd [[highlight default link CmpItemAbbr CmpItemAbbrDefault]] -end - -if vim.fn.hlexists('CmpItemAbbrDeprecated') ~= 1 then - vim.cmd [[highlight default link CmpItemAbbrDeprecated CmpItemAbbrDeprecatedDefault]] -end - -if vim.fn.hlexists('CmpItemAbbrMatch') ~= 1 then - vim.cmd [[highlight default link CmpItemAbbrMatch CmpItemAbbrMatchDefault]] -end - -if vim.fn.hlexists('CmpItemAbbrMatchFuzzy') ~= 1 then - vim.cmd [[highlight default link CmpItemAbbrMatchFuzzy CmpItemAbbrMatchFuzzyDefault]] -end - -if vim.fn.hlexists('CmpItemKind') ~= 1 then - vim.cmd [[highlight default link CmpItemKind CmpItemKindDefault]] -end -for name in pairs(types.lsp.CompletionItemKind) do - if type(name) == 'string' then - local hi = ('CmpItemKind%s'):format(name) - if vim.fn.hlexists(hi) ~= 1 then - vim.cmd(([[highlight default link %s %sDefault]]):format(hi, hi)) - end - end -end - -if vim.fn.hlexists('CmpItemMenu') ~= 1 then - vim.cmd [[highlight default link CmpItemMenu CmpItemMenuDefault]] -end - -vim.cmd [[command! CmpStatus lua require('cmp').status()]] - -vim.cmd [[doautocmd User CmpReady]] +autocmd.emit('ColorScheme') if vim.on_key then vim.on_key(function(keys) if keys == vim.api.nvim_replace_termcodes('', true, true, true) then vim.schedule(function() if not api.is_suitable_mode() then - require('cmp.utils.autocmd').emit('InsertLeave') + autocmd.emit('InsertLeave') end end) end end, vim.api.nvim_create_namespace('cmp.plugin')) end +vim.api.nvim_create_user_command('CmpStatus', function() + require('cmp').status() +end, { desc = 'Check status of cmp sources' }) + +vim.cmd([[doautocmd User CmpReady]]) diff --git a/bundle/nvim-cmp/utils/install_stylua.sh b/bundle/nvim-cmp/utils/install_stylua.sh deleted file mode 100644 index 963416ea0..000000000 --- a/bundle/nvim-cmp/utils/install_stylua.sh +++ /dev/null @@ -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 "$@" diff --git a/bundle/nvim-cmp/utils/vimrc.vim b/bundle/nvim-cmp/utils/vimrc.vim index a83e71d31..a18e3c36e 100644 --- a/bundle/nvim-cmp/utils/vimrc.vim +++ b/bundle/nvim-cmp/utils/vimrc.vim @@ -36,15 +36,15 @@ cmp.setup { [''] = cmp.mapping.confirm({ select = true }) }, - sources = { + sources = cmp.config.sources({ { name = "nvim_lsp" }, { name = "buffer" }, - }, + }), } EOF lua << EOF -local capabilities = require('cmp_nvim_lsp').update_capabilities(vim.lsp.protocol.make_client_capabilities()) +local capabilities = require('cmp_nvim_lsp').default_capabilities() require'lspconfig'.cssls.setup { capabilities = capabilities,