mirror of
https://github.com/hrsh7th/nvim-cmp
synced 2024-09-16 20:54:03 +02:00
dev (#1)
* dev * Improve sync design * Support buffer local mapping * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * stylua * tmp * tmp * tmp * tmp * tmp * tmp * tmp * integration * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * update * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp * tmp
This commit is contained in:
parent
b32a6e7e77
commit
d23d3533cf
53 changed files with 4681 additions and 0 deletions
6
.githooks/pre-commit
Executable file
6
.githooks/pre-commit
Executable file
|
@ -0,0 +1,6 @@
|
|||
#!/bin/sh
|
||||
|
||||
DIR="$(dirname $(dirname $( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )))"
|
||||
|
||||
cd $DIR
|
||||
make pre-commit
|
49
.github/workflows/integration.yaml
vendored
Normal file
49
.github/workflows/integration.yaml
vendored
Normal file
|
@ -0,0 +1,49 @@
|
|||
name: integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- 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:
|
||||
neovim: true
|
||||
|
||||
- name: Setup lua
|
||||
uses: leafo/gh-actions-lua@v8
|
||||
with:
|
||||
luaVersion: "luajit-2.1.0-beta3"
|
||||
|
||||
- name: Setup luarocks
|
||||
uses: leafo/gh-actions-luarocks@v4
|
||||
|
||||
- name: Setup tools
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install stylua
|
||||
luarocks install luacheck
|
||||
luarocks install vusted
|
||||
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
run: make integration
|
||||
|
2
.luacheckrc
Normal file
2
.luacheckrc
Normal file
|
@ -0,0 +1,2 @@
|
|||
globals = { 'vim', 'describe', 'it', 'before_each', 'after_each', 'assert', 'async' }
|
||||
max_line_length = false
|
24
Makefile
Normal file
24
Makefile
Normal file
|
@ -0,0 +1,24 @@
|
|||
.PHONY: fmt
|
||||
fmt:
|
||||
stylua --glob lua/**/*.lua -- lua
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
luacheck ./lua
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
vusted ./lua
|
||||
|
||||
.PHONY: pre-commit
|
||||
pre-commit:
|
||||
stylua --check --glob lua/**/*.lua -- lua
|
||||
luacheck lua
|
||||
vusted lua
|
||||
|
||||
.PHONY: integration
|
||||
integration:
|
||||
stylua --check --glob lua/**/*.lua -- lua
|
||||
luacheck lua
|
||||
vusted lua
|
||||
|
91
README.md
91
README.md
|
@ -1,2 +1,93 @@
|
|||
# nvim-cmp
|
||||
|
||||
A completion plugin for neovim written in Lua.
|
||||
|
||||
|
||||
Status
|
||||
====================
|
||||
|
||||
design and development
|
||||
|
||||
|
||||
Development
|
||||
====================
|
||||
|
||||
You should read [type definitions](/lua/cmp/types) and [LSP spec](https://microsoft.github.io/language-server-protocol/specifications/specification-current/) to develop core or sources.
|
||||
|
||||
### Overview
|
||||
|
||||
`nvim-cmp` emphasizes compatibility with the VSCode behavior and the LSP specification but there are some little differences.
|
||||
|
||||
1. In `nvim-cmp`, the `CompletionItem` can have `word` and `dup` property that introduced by vim's completion mechanism.
|
||||
|
||||
|
||||
### Create custom source
|
||||
|
||||
The example source is here.
|
||||
|
||||
- The `complete` function is required but others can be omitted.
|
||||
- The `callback` argument must always be called.
|
||||
|
||||
```lua
|
||||
local source = {}
|
||||
|
||||
---Create source.
|
||||
source.new = function()
|
||||
local self = setmetatable({}, { __index = source })
|
||||
self.your_awesome_variable = 1
|
||||
return self
|
||||
end
|
||||
|
||||
---Return keyword pattern which will be used by the followings.
|
||||
--- 1. Trigger keyword completion
|
||||
--- 2. Detect menu start offset
|
||||
--- 3. Reset completion state
|
||||
---@return string
|
||||
function source:get_keyword_pattern()
|
||||
return '???'
|
||||
end
|
||||
|
||||
---Return trigger characters.
|
||||
---@return string[]
|
||||
function source:get_trigger_characters()
|
||||
return { ??? }
|
||||
end
|
||||
|
||||
---Invoke completion.
|
||||
---@param request cmp.CompletionRequest
|
||||
---@param callback fun(response: lsp.CompletionResponse|nil)
|
||||
---NOTE: This method is required.
|
||||
function source:complete(request, callback)
|
||||
callback({
|
||||
{ label = 'January' },
|
||||
{ label = 'February' },
|
||||
{ label = 'March' },
|
||||
{ label = 'April' },
|
||||
{ label = 'May' },
|
||||
{ label = 'June' },
|
||||
{ label = 'July' },
|
||||
{ label = 'August' },
|
||||
{ label = 'September' },
|
||||
{ label = 'October' },
|
||||
{ label = 'November' },
|
||||
{ label = 'December' },
|
||||
})
|
||||
end
|
||||
|
||||
---Resolve completion item that will be called when the item selected or before the item confirmation.
|
||||
---@param completion_item lsp.CompletionItem
|
||||
---@param callback fun(completion_item: lsp.CompletionItem|nil)
|
||||
function source:resolve(completion_item, callback)
|
||||
callback(completion_item)
|
||||
end
|
||||
|
||||
---Execute command that will be called when after the item confirmation.
|
||||
---@param completion_item lsp.CompletionItem
|
||||
---@param callback fun(completion_item: lsp.CompletionItem|nil)
|
||||
function source:execute(completion_item, callback)
|
||||
callback(completion_item)
|
||||
end
|
||||
|
||||
return source
|
||||
```
|
||||
|
||||
|
|
32
autoload/cmp.vim
Normal file
32
autoload/cmp.vim
Normal file
|
@ -0,0 +1,32 @@
|
|||
let s:Position = vital#cmp#import('VS.LSP.Position')
|
||||
let s:TextEdit = vital#cmp#import('VS.LSP.TextEdit')
|
||||
let s:CompletionItem = vital#cmp#import('VS.LSP.CompletionItem')
|
||||
|
||||
"
|
||||
" cmp#apply_text_edits
|
||||
"
|
||||
function! cmp#apply_text_edits(bufnr, text_edits) abort
|
||||
call s:TextEdit.apply(a:bufnr, a:text_edits)
|
||||
endfunction
|
||||
|
||||
"
|
||||
" cmp#confirm
|
||||
"
|
||||
function! cmp#confirm(args) abort
|
||||
call s:CompletionItem.confirm({
|
||||
\ 'suggest_position': s:Position.vim_to_lsp('%', [line('.'), a:args.suggest_offset]),
|
||||
\ 'request_position': s:Position.vim_to_lsp('%', [line('.'), a:args.request_offset]),
|
||||
\ 'current_position': s:Position.vim_to_lsp('%', [line('.'), col('.')]),
|
||||
\ 'current_line': getline('.'),
|
||||
\ 'completion_item': a:args.completion_item,
|
||||
\ 'expand_snippet': s:get_expand_snippet(),
|
||||
\ })
|
||||
endfunction
|
||||
|
||||
"
|
||||
" get_expand_snippet
|
||||
"
|
||||
function! s:get_expand_snippet() abort
|
||||
return { args -> luaeval('require"cmp"._expand_snippet(_A)', args) }
|
||||
endfunction
|
||||
|
9
autoload/vital/_cmp.vim
Normal file
9
autoload/vital/_cmp.vim
Normal file
|
@ -0,0 +1,9 @@
|
|||
let s:_plugin_name = expand('<sfile>:t:r')
|
||||
|
||||
function! vital#{s:_plugin_name}#new() abort
|
||||
return vital#{s:_plugin_name[1:]}#new()
|
||||
endfunction
|
||||
|
||||
function! vital#{s:_plugin_name}#function(funcname) abort
|
||||
silent! return function(a:funcname)
|
||||
endfunction
|
178
autoload/vital/_cmp/VS/LSP/CompletionItem.vim
Normal file
178
autoload/vital/_cmp/VS/LSP/CompletionItem.vim
Normal file
|
@ -0,0 +1,178 @@
|
|||
" ___vital___
|
||||
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
|
||||
" Do not modify the code nor insert new lines before '" ___vital___'
|
||||
function! s:_SID() abort
|
||||
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
|
||||
endfunction
|
||||
execute join(['function! vital#_cmp#VS#LSP#CompletionItem#import() abort', printf("return map({'_vital_depends': '', 'confirm': '', '_vital_loaded': ''}, \"vital#_cmp#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
|
||||
delfunction s:_SID
|
||||
" ___vital___
|
||||
"
|
||||
" _vital_loaded
|
||||
"
|
||||
function! s:_vital_loaded(V) abort
|
||||
let s:Position = a:V.import('VS.LSP.Position')
|
||||
let s:TextEdit = a:V.import('VS.LSP.TextEdit')
|
||||
let s:Text = a:V.import('VS.LSP.Text')
|
||||
endfunction
|
||||
|
||||
"
|
||||
" _vital_depends
|
||||
"
|
||||
function! s:_vital_depends() abort
|
||||
return ['VS.LSP.Position', 'VS.LSP.TextEdit', 'VS.LSP.Text']
|
||||
endfunction
|
||||
|
||||
"
|
||||
" confirm
|
||||
"
|
||||
" @param {LSP.Position} args.suggest_position
|
||||
" @param {LSP.Position} args.request_position
|
||||
" @param {LSP.Position} args.current_position
|
||||
" @param {string} args.current_line
|
||||
" @param {LSP.CompletionItem} args.completion_item
|
||||
" @param {(args: { body: string; insert_text_mode: number; }) => void?} args.expand_snippet
|
||||
"
|
||||
" # Pre-condition
|
||||
"
|
||||
" - You must pass `current_position` that represents the position when `CompleteDone` was fired.
|
||||
" - You must pass `current_line` that represents the line when `CompleteDone` was fired.
|
||||
" - You must call this function after the commit characters has been inserted.
|
||||
"
|
||||
" # The positoins
|
||||
"
|
||||
" 0. The example case
|
||||
"
|
||||
" call getbufl|<C-x><C-o><C-n><C-y> -> call getbufline|
|
||||
"
|
||||
" 1. suggest_position
|
||||
"
|
||||
" call |getbufline
|
||||
"
|
||||
" 2. request_position
|
||||
"
|
||||
" call getbufl|ine
|
||||
"
|
||||
" 3. current_position
|
||||
"
|
||||
" call getbufline|
|
||||
"
|
||||
"
|
||||
function! s:confirm(args) abort
|
||||
let l:suggest_position = a:args.suggest_position
|
||||
let l:request_position = a:args.request_position
|
||||
let l:current_position = a:args.current_position
|
||||
let l:current_line = a:args.current_line
|
||||
let l:completion_item = a:args.completion_item
|
||||
let l:ExpandSnippet = get(a:args, 'expand_snippet', v:null)
|
||||
|
||||
" 1. Prepare for alignment to VSCode behavior.
|
||||
let l:expansion = s:_get_expansion({
|
||||
\ 'suggest_position': l:suggest_position,
|
||||
\ 'request_position': l:request_position,
|
||||
\ 'current_position': l:current_position,
|
||||
\ 'current_line': l:current_line,
|
||||
\ 'completion_item': l:completion_item,
|
||||
\ })
|
||||
if !empty(l:expansion)
|
||||
" Remove commit characters if expansion is needed.
|
||||
if getline('.') !=# l:current_line
|
||||
call setline(l:current_position.line + 1, l:current_line)
|
||||
call cursor(s:Position.lsp_to_vim('%', l:current_position))
|
||||
endif
|
||||
|
||||
" Restore state of the timing when `textDocument/completion` was sent.
|
||||
call s:TextEdit.apply('%', [{
|
||||
\ 'range': { 'start': l:request_position, 'end': l:current_position },
|
||||
\ 'newText': ''
|
||||
\ }])
|
||||
endif
|
||||
|
||||
" 2. Apply additionalTextEdits
|
||||
if type(get(l:completion_item, 'additionalTextEdits', v:null)) == type([])
|
||||
call s:TextEdit.apply('%', l:completion_item.additionalTextEdits)
|
||||
endif
|
||||
|
||||
" 3. Apply expansion
|
||||
if !empty(l:expansion)
|
||||
let l:current_position = s:Position.cursor() " Update current_position to after additionalTextEdits.
|
||||
let l:range = {
|
||||
\ 'start': extend({
|
||||
\ 'character': l:current_position.character - l:expansion.overflow_before,
|
||||
\ }, l:current_position, 'keep'),
|
||||
\ 'end': extend({
|
||||
\ 'character': l:current_position.character + l:expansion.overflow_after,
|
||||
\ }, l:current_position, 'keep')
|
||||
\ }
|
||||
|
||||
" Snippet.
|
||||
if l:expansion.is_snippet && !empty(l:ExpandSnippet)
|
||||
call s:TextEdit.apply('%', [{ 'range': l:range, 'newText': '' }])
|
||||
call cursor(s:Position.lsp_to_vim('%', l:range.start))
|
||||
call l:ExpandSnippet({ 'body': l:expansion.new_text, 'insert_text_mode': get(l:completion_item, 'insertTextMode', 2) })
|
||||
|
||||
" TextEdit.
|
||||
else
|
||||
call s:TextEdit.apply('%', [{ 'range': l:range, 'newText': l:expansion.new_text }])
|
||||
|
||||
" Move cursor position to end of new_text like as snippet.
|
||||
let l:lines = s:Text.split_by_eol(l:expansion.new_text)
|
||||
let l:cursor = copy(l:range.start)
|
||||
let l:cursor.line += len(l:lines) - 1
|
||||
let l:cursor.character = strchars(l:lines[-1]) + (len(l:lines) == 1 ? l:cursor.character : 0)
|
||||
call cursor(s:Position.lsp_to_vim('%', l:cursor))
|
||||
endif
|
||||
endif
|
||||
endfunction
|
||||
|
||||
"
|
||||
" _get_expansion
|
||||
"
|
||||
function! s:_get_expansion(args) abort
|
||||
let l:current_line = a:args.current_line
|
||||
let l:suggest_position = a:args.suggest_position
|
||||
let l:request_position = a:args.request_position
|
||||
let l:current_position = a:args.current_position
|
||||
let l:completion_item = a:args.completion_item
|
||||
|
||||
let l:is_snippet = get(l:completion_item, 'insertTextFormat', 1) == 2
|
||||
if type(get(l:completion_item, 'textEdit', v:null)) == type({})
|
||||
let l:inserted_text = strcharpart(l:current_line, l:request_position.character, l:current_position.character - l:request_position.character)
|
||||
let l:overflow_before = l:request_position.character - l:completion_item.textEdit.range.start.character
|
||||
let l:overflow_after = l:completion_item.textEdit.range.end.character - l:request_position.character
|
||||
let l:inserted = ''
|
||||
\ . strcharpart(l:current_line, l:request_position.character - l:overflow_before, l:overflow_before)
|
||||
\ . strcharpart(l:current_line, l:request_position.character, strchars(l:inserted_text) + l:overflow_after)
|
||||
let l:new_text = l:completion_item.textEdit.newText
|
||||
if s:_trim_tabstop(l:new_text) !=# l:inserted
|
||||
" The LSP spec says `textEdit range must contain the request position.`
|
||||
return {
|
||||
\ 'overflow_before': max([0, l:overflow_before]),
|
||||
\ 'overflow_after': max([0, l:overflow_after]),
|
||||
\ 'new_text': l:new_text,
|
||||
\ 'is_snippet': l:is_snippet,
|
||||
\ }
|
||||
endif
|
||||
else
|
||||
let l:inserted = strcharpart(l:current_line, l:suggest_position.character, l:current_position.character - l:suggest_position.character)
|
||||
let l:new_text = get(l:completion_item, 'insertText', v:null)
|
||||
let l:new_text = !empty(l:new_text) ? l:new_text : l:completion_item.label
|
||||
if s:_trim_tabstop(l:new_text) !=# l:inserted
|
||||
return {
|
||||
\ 'overflow_before': l:request_position.character - l:suggest_position.character,
|
||||
\ 'overflow_after': 0,
|
||||
\ 'new_text': l:new_text,
|
||||
\ 'is_snippet': l:is_snippet,
|
||||
\ }
|
||||
endif
|
||||
endif
|
||||
return {}
|
||||
endfunction
|
||||
|
||||
"
|
||||
" _trim_tabstop
|
||||
"
|
||||
function! s:_trim_tabstop(text) abort
|
||||
return substitute(a:text, '\%(\$0\|\${0}\)$', '', 'g')
|
||||
endfunction
|
||||
|
62
autoload/vital/_cmp/VS/LSP/Position.vim
Normal file
62
autoload/vital/_cmp/VS/LSP/Position.vim
Normal file
|
@ -0,0 +1,62 @@
|
|||
" ___vital___
|
||||
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
|
||||
" Do not modify the code nor insert new lines before '" ___vital___'
|
||||
function! s:_SID() abort
|
||||
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
|
||||
endfunction
|
||||
execute join(['function! vital#_cmp#VS#LSP#Position#import() abort', printf("return map({'cursor': '', 'vim_to_lsp': '', 'lsp_to_vim': ''}, \"vital#_cmp#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
|
||||
delfunction s:_SID
|
||||
" ___vital___
|
||||
"
|
||||
" cursor
|
||||
"
|
||||
function! s:cursor() abort
|
||||
return s:vim_to_lsp('%', getpos('.')[1 : 3])
|
||||
endfunction
|
||||
|
||||
"
|
||||
" vim_to_lsp
|
||||
"
|
||||
function! s:vim_to_lsp(expr, pos) abort
|
||||
let l:line = s:_get_buffer_line(a:expr, a:pos[0])
|
||||
if l:line is v:null
|
||||
return {
|
||||
\ 'line': a:pos[0] - 1,
|
||||
\ 'character': a:pos[1] - 1
|
||||
\ }
|
||||
endif
|
||||
|
||||
return {
|
||||
\ 'line': a:pos[0] - 1,
|
||||
\ 'character': strchars(strpart(l:line, 0, a:pos[1] - 1))
|
||||
\ }
|
||||
endfunction
|
||||
|
||||
"
|
||||
" lsp_to_vim
|
||||
"
|
||||
function! s:lsp_to_vim(expr, position) abort
|
||||
let l:line = s:_get_buffer_line(a:expr, a:position.line + 1)
|
||||
if l:line is v:null
|
||||
return [a:position.line + 1, a:position.character + 1]
|
||||
endif
|
||||
return [a:position.line + 1, byteidx(l:line, a:position.character) + 1]
|
||||
endfunction
|
||||
|
||||
"
|
||||
" _get_buffer_line
|
||||
"
|
||||
function! s:_get_buffer_line(expr, lnum) abort
|
||||
try
|
||||
let l:expr = bufnr(a:expr)
|
||||
catch /.*/
|
||||
let l:expr = a:expr
|
||||
endtry
|
||||
if bufloaded(l:expr)
|
||||
return get(getbufline(l:expr, a:lnum), 0, v:null)
|
||||
elseif filereadable(a:expr)
|
||||
return get(readfile(a:expr, '', a:lnum), 0, v:null)
|
||||
endif
|
||||
return v:null
|
||||
endfunction
|
||||
|
23
autoload/vital/_cmp/VS/LSP/Text.vim
Normal file
23
autoload/vital/_cmp/VS/LSP/Text.vim
Normal file
|
@ -0,0 +1,23 @@
|
|||
" ___vital___
|
||||
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
|
||||
" Do not modify the code nor insert new lines before '" ___vital___'
|
||||
function! s:_SID() abort
|
||||
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
|
||||
endfunction
|
||||
execute join(['function! vital#_cmp#VS#LSP#Text#import() abort', printf("return map({'normalize_eol': '', 'split_by_eol': ''}, \"vital#_cmp#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
|
||||
delfunction s:_SID
|
||||
" ___vital___
|
||||
"
|
||||
" normalize_eol
|
||||
"
|
||||
function! s:normalize_eol(text) abort
|
||||
return substitute(a:text, "\r\n\\|\r", "\n", 'g')
|
||||
endfunction
|
||||
|
||||
"
|
||||
" split_by_eol
|
||||
"
|
||||
function! s:split_by_eol(text) abort
|
||||
return split(a:text, "\r\n\\|\r\\|\n", v:true)
|
||||
endfunction
|
||||
|
185
autoload/vital/_cmp/VS/LSP/TextEdit.vim
Normal file
185
autoload/vital/_cmp/VS/LSP/TextEdit.vim
Normal file
|
@ -0,0 +1,185 @@
|
|||
" ___vital___
|
||||
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
|
||||
" Do not modify the code nor insert new lines before '" ___vital___'
|
||||
function! s:_SID() abort
|
||||
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
|
||||
endfunction
|
||||
execute join(['function! vital#_cmp#VS#LSP#TextEdit#import() abort', printf("return map({'_vital_depends': '', 'apply': '', '_vital_loaded': ''}, \"vital#_cmp#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
|
||||
delfunction s:_SID
|
||||
" ___vital___
|
||||
"
|
||||
" _vital_loaded
|
||||
"
|
||||
function! s:_vital_loaded(V) abort
|
||||
let s:Text = a:V.import('VS.LSP.Text')
|
||||
let s:Position = a:V.import('VS.LSP.Position')
|
||||
let s:Buffer = a:V.import('VS.Vim.Buffer')
|
||||
let s:Option = a:V.import('VS.Vim.Option')
|
||||
endfunction
|
||||
|
||||
"
|
||||
" _vital_depends
|
||||
"
|
||||
function! s:_vital_depends() abort
|
||||
return ['VS.LSP.Text', 'VS.LSP.Position', 'VS.Vim.Buffer', 'VS.Vim.Option']
|
||||
endfunction
|
||||
|
||||
"
|
||||
" apply
|
||||
"
|
||||
function! s:apply(path, text_edits) abort
|
||||
let l:current_bufname = bufname('%')
|
||||
let l:current_position = s:Position.cursor()
|
||||
|
||||
let l:target_bufnr = s:_switch(a:path)
|
||||
call s:_substitute(l:target_bufnr, a:text_edits, l:current_position)
|
||||
let l:current_bufnr = s:_switch(l:current_bufname)
|
||||
|
||||
if l:current_bufnr == l:target_bufnr
|
||||
call cursor(s:Position.lsp_to_vim('%', l:current_position))
|
||||
endif
|
||||
endfunction
|
||||
|
||||
"
|
||||
" _substitute
|
||||
"
|
||||
function! s:_substitute(bufnr, text_edits, current_position) abort
|
||||
try
|
||||
" Save state.
|
||||
let l:Restore = s:Option.define({
|
||||
\ 'foldenable': '0',
|
||||
\ })
|
||||
let l:view = winsaveview()
|
||||
|
||||
" Apply substitute.
|
||||
let [l:fixeol, l:text_edits] = s:_normalize(a:bufnr, a:text_edits)
|
||||
for l:text_edit in l:text_edits
|
||||
let l:start = s:Position.lsp_to_vim(a:bufnr, l:text_edit.range.start)
|
||||
let l:end = s:Position.lsp_to_vim(a:bufnr, l:text_edit.range.end)
|
||||
let l:text = s:Text.normalize_eol(l:text_edit.newText)
|
||||
execute printf('noautocmd keeppatterns keepjumps silent %ssubstitute/\%%%sl\%%%sc\_.\{-}\%%%sl\%%%sc/\=l:text/%se',
|
||||
\ l:start[0],
|
||||
\ l:start[0],
|
||||
\ l:start[1],
|
||||
\ l:end[0],
|
||||
\ l:end[1],
|
||||
\ &gdefault ? 'g' : ''
|
||||
\ )
|
||||
call s:_fix_cursor_position(a:current_position, l:text_edit, s:Text.split_by_eol(l:text))
|
||||
endfor
|
||||
|
||||
" Remove last empty line if fixeol enabled.
|
||||
if l:fixeol && getline('$') ==# ''
|
||||
noautocmd keeppatterns keepjumps silent $delete _
|
||||
endif
|
||||
catch /.*/
|
||||
echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint })
|
||||
finally
|
||||
" Restore state.
|
||||
call l:Restore()
|
||||
call winrestview(l:view)
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
"
|
||||
" _fix_cursor_position
|
||||
"
|
||||
function! s:_fix_cursor_position(position, text_edit, lines) abort
|
||||
let l:lines_len = len(a:lines)
|
||||
let l:range_len = (a:text_edit.range.end.line - a:text_edit.range.start.line) + 1
|
||||
|
||||
if a:text_edit.range.end.line < a:position.line
|
||||
let a:position.line += l:lines_len - l:range_len
|
||||
elseif a:text_edit.range.end.line == a:position.line && a:text_edit.range.end.character <= a:position.character
|
||||
let a:position.line += l:lines_len - l:range_len
|
||||
let a:position.character = strchars(a:lines[-1]) + (a:position.character - a:text_edit.range.end.character)
|
||||
if l:lines_len == 1
|
||||
let a:position.character += a:text_edit.range.start.character
|
||||
endif
|
||||
endif
|
||||
endfunction
|
||||
|
||||
"
|
||||
" _normalize
|
||||
"
|
||||
function! s:_normalize(bufnr, text_edits) abort
|
||||
let l:text_edits = type(a:text_edits) == type([]) ? a:text_edits : [a:text_edits]
|
||||
let l:text_edits = s:_range(l:text_edits)
|
||||
let l:text_edits = sort(l:text_edits, function('s:_compare'))
|
||||
let l:text_edits = reverse(l:text_edits)
|
||||
return s:_fix_text_edits(a:bufnr, l:text_edits)
|
||||
endfunction
|
||||
|
||||
"
|
||||
" _range
|
||||
"
|
||||
function! s:_range(text_edits) abort
|
||||
let l:text_edits = []
|
||||
for l:text_edit in a:text_edits
|
||||
if type(l:text_edit) != type({})
|
||||
continue
|
||||
endif
|
||||
if l:text_edit.range.start.line > l:text_edit.range.end.line || (
|
||||
\ l:text_edit.range.start.line == l:text_edit.range.end.line &&
|
||||
\ l:text_edit.range.start.character > l:text_edit.range.end.character
|
||||
\ )
|
||||
let l:text_edit.range = { 'start': l:text_edit.range.end, 'end': l:text_edit.range.start }
|
||||
endif
|
||||
let l:text_edits += [l:text_edit]
|
||||
endfor
|
||||
return l:text_edits
|
||||
endfunction
|
||||
|
||||
"
|
||||
" _compare
|
||||
"
|
||||
function! s:_compare(text_edit1, text_edit2) abort
|
||||
let l:diff = a:text_edit1.range.start.line - a:text_edit2.range.start.line
|
||||
if l:diff == 0
|
||||
return a:text_edit1.range.start.character - a:text_edit2.range.start.character
|
||||
endif
|
||||
return l:diff
|
||||
endfunction
|
||||
|
||||
"
|
||||
" _fix_text_edits
|
||||
"
|
||||
function! s:_fix_text_edits(bufnr, text_edits) abort
|
||||
let l:max = s:Buffer.get_line_count(a:bufnr)
|
||||
|
||||
let l:fixeol = v:false
|
||||
let l:text_edits = []
|
||||
for l:text_edit in a:text_edits
|
||||
if l:max <= l:text_edit.range.start.line
|
||||
let l:text_edit.range.start.line = l:max - 1
|
||||
let l:text_edit.range.start.character = strchars(get(getbufline(a:bufnr, '$'), 0, ''))
|
||||
let l:text_edit.newText = "\n" . l:text_edit.newText
|
||||
let l:fixeol = &fixendofline && !&binary
|
||||
endif
|
||||
if l:max <= l:text_edit.range.end.line
|
||||
let l:text_edit.range.end.line = l:max - 1
|
||||
let l:text_edit.range.end.character = strchars(get(getbufline(a:bufnr, '$'), 0, ''))
|
||||
let l:fixeol = &fixendofline && !&binary
|
||||
endif
|
||||
call add(l:text_edits, l:text_edit)
|
||||
endfor
|
||||
|
||||
return [l:fixeol, l:text_edits]
|
||||
endfunction
|
||||
|
||||
"
|
||||
" _switch
|
||||
"
|
||||
function! s:_switch(path) abort
|
||||
let l:curr = bufnr('%')
|
||||
let l:next = bufnr(a:path)
|
||||
if l:next >= 0
|
||||
if l:curr != l:next
|
||||
execute printf('noautocmd keepalt keepjumps %sbuffer!', bufnr(a:path))
|
||||
endif
|
||||
else
|
||||
execute printf('noautocmd keepalt keepjumps edit! %s', fnameescape(a:path))
|
||||
endif
|
||||
return bufnr('%')
|
||||
endfunction
|
||||
|
126
autoload/vital/_cmp/VS/Vim/Buffer.vim
Normal file
126
autoload/vital/_cmp/VS/Vim/Buffer.vim
Normal file
|
@ -0,0 +1,126 @@
|
|||
" ___vital___
|
||||
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
|
||||
" Do not modify the code nor insert new lines before '" ___vital___'
|
||||
function! s:_SID() abort
|
||||
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
|
||||
endfunction
|
||||
execute join(['function! vital#_cmp#VS#Vim#Buffer#import() abort', printf("return map({'get_line_count': '', 'do': '', 'create': '', 'pseudo': '', 'ensure': '', 'load': ''}, \"vital#_cmp#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
|
||||
delfunction s:_SID
|
||||
" ___vital___
|
||||
let s:Do = { -> {} }
|
||||
|
||||
let g:___VS_Vim_Buffer_id = get(g:, '___VS_Vim_Buffer_id', 0)
|
||||
|
||||
"
|
||||
" get_line_count
|
||||
"
|
||||
if exists('*nvim_buf_line_count')
|
||||
function! s:get_line_count(bufnr) abort
|
||||
return nvim_buf_line_count(a:bufnr)
|
||||
endfunction
|
||||
elseif has('patch-8.2.0019')
|
||||
function! s:get_line_count(bufnr) abort
|
||||
return getbufinfo(a:bufnr)[0].linecount
|
||||
endfunction
|
||||
else
|
||||
function! s:get_line_count(bufnr) abort
|
||||
if bufnr('%') == bufnr(a:bufnr)
|
||||
return line('$')
|
||||
endif
|
||||
return len(getbufline(a:bufnr, '^', '$'))
|
||||
endfunction
|
||||
endif
|
||||
|
||||
"
|
||||
" create
|
||||
"
|
||||
function! s:create(...) abort
|
||||
let g:___VS_Vim_Buffer_id += 1
|
||||
let l:bufname = printf('VS.Vim.Buffer: %s: %s',
|
||||
\ g:___VS_Vim_Buffer_id,
|
||||
\ get(a:000, 0, 'VS.Vim.Buffer.Default')
|
||||
\ )
|
||||
return s:load(l:bufname)
|
||||
endfunction
|
||||
|
||||
"
|
||||
" ensure
|
||||
"
|
||||
function! s:ensure(expr) abort
|
||||
if !bufexists(a:expr)
|
||||
if type(a:expr) == type(0)
|
||||
throw printf('VS.Vim.Buffer: `%s` is not valid expr.', l:bufnr)
|
||||
endif
|
||||
badd `=a:expr`
|
||||
endif
|
||||
return bufnr(a:expr)
|
||||
endfunction
|
||||
|
||||
"
|
||||
" load
|
||||
"
|
||||
if exists('*bufload')
|
||||
function! s:load(expr) abort
|
||||
let l:bufnr = s:ensure(a:expr)
|
||||
if !bufloaded(l:bufnr)
|
||||
call bufload(l:bufnr)
|
||||
endif
|
||||
return l:bufnr
|
||||
endfunction
|
||||
else
|
||||
function! s:load(expr) abort
|
||||
let l:curr_bufnr = bufnr('%')
|
||||
try
|
||||
let l:bufnr = s:ensure(a:expr)
|
||||
execute printf('keepalt keepjumps silent %sbuffer', l:bufnr)
|
||||
catch /.*/
|
||||
echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint })
|
||||
finally
|
||||
execute printf('noautocmd keepalt keepjumps silent %sbuffer', l:curr_bufnr)
|
||||
endtry
|
||||
return l:bufnr
|
||||
endfunction
|
||||
endif
|
||||
|
||||
"
|
||||
" do
|
||||
"
|
||||
function! s:do(bufnr, func) abort
|
||||
let l:curr_bufnr = bufnr('%')
|
||||
if l:curr_bufnr == a:bufnr
|
||||
call a:func()
|
||||
return
|
||||
endif
|
||||
|
||||
try
|
||||
execute printf('noautocmd keepalt keepjumps silent %sbuffer', a:bufnr)
|
||||
call a:func()
|
||||
catch /.*/
|
||||
echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint })
|
||||
finally
|
||||
execute printf('noautocmd keepalt keepjumps silent %sbuffer', l:curr_bufnr)
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
"
|
||||
" pseudo
|
||||
"
|
||||
function! s:pseudo(filepath) abort
|
||||
if !filereadable(a:filepath)
|
||||
throw printf('VS.Vim.Buffer: `%s` is not valid filepath.', a:filepath)
|
||||
endif
|
||||
|
||||
" create pseudo buffer
|
||||
let l:bufname = printf('VSVimBufferPseudo://%s', a:filepath)
|
||||
if bufexists(l:bufname)
|
||||
return s:ensure(l:bufname)
|
||||
endif
|
||||
|
||||
let l:bufnr = s:ensure(l:bufname)
|
||||
let l:group = printf('VS_Vim_Buffer_pseudo:%s', l:bufnr)
|
||||
execute printf('augroup %s', l:group)
|
||||
execute printf('autocmd BufReadCmd <buffer=%s> call setline(1, readfile(bufname("%")[20 : -1])) | try | filetype detect | catch /.*/ | endtry | augroup %s | autocmd! | augroup END', l:bufnr, l:group)
|
||||
augroup END
|
||||
return l:bufnr
|
||||
endfunction
|
||||
|
21
autoload/vital/_cmp/VS/Vim/Option.vim
Normal file
21
autoload/vital/_cmp/VS/Vim/Option.vim
Normal file
|
@ -0,0 +1,21 @@
|
|||
" ___vital___
|
||||
" NOTE: lines between '" ___vital___' is generated by :Vitalize.
|
||||
" Do not modify the code nor insert new lines before '" ___vital___'
|
||||
function! s:_SID() abort
|
||||
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
|
||||
endfunction
|
||||
execute join(['function! vital#_cmp#VS#Vim#Option#import() abort', printf("return map({'define': ''}, \"vital#_cmp#function('<SNR>%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n")
|
||||
delfunction s:_SID
|
||||
" ___vital___
|
||||
"
|
||||
" define
|
||||
"
|
||||
function! s:define(map) abort
|
||||
let l:old = {}
|
||||
for [l:key, l:value] in items(a:map)
|
||||
let l:old[l:key] = eval(printf('&%s', l:key))
|
||||
execute printf('let &%s = "%s"', l:key, l:value)
|
||||
endfor
|
||||
return { -> s:define(l:old) }
|
||||
endfunction
|
||||
|
330
autoload/vital/cmp.vim
Normal file
330
autoload/vital/cmp.vim
Normal file
|
@ -0,0 +1,330 @@
|
|||
let s:plugin_name = expand('<sfile>:t:r')
|
||||
let s:vital_base_dir = expand('<sfile>:h')
|
||||
let s:project_root = expand('<sfile>:h:h:h')
|
||||
let s:is_vital_vim = s:plugin_name is# 'vital'
|
||||
|
||||
let s:loaded = {}
|
||||
let s:cache_sid = {}
|
||||
|
||||
function! vital#{s:plugin_name}#new() abort
|
||||
return s:new(s:plugin_name)
|
||||
endfunction
|
||||
|
||||
function! vital#{s:plugin_name}#import(...) abort
|
||||
if !exists('s:V')
|
||||
let s:V = s:new(s:plugin_name)
|
||||
endif
|
||||
return call(s:V.import, a:000, s:V)
|
||||
endfunction
|
||||
|
||||
let s:Vital = {}
|
||||
|
||||
function! s:new(plugin_name) abort
|
||||
let base = deepcopy(s:Vital)
|
||||
let base._plugin_name = a:plugin_name
|
||||
return base
|
||||
endfunction
|
||||
|
||||
function! s:vital_files() abort
|
||||
if !exists('s:vital_files')
|
||||
let s:vital_files = map(
|
||||
\ s:is_vital_vim ? s:_global_vital_files() : s:_self_vital_files(),
|
||||
\ 'fnamemodify(v:val, ":p:gs?[\\\\/]?/?")')
|
||||
endif
|
||||
return copy(s:vital_files)
|
||||
endfunction
|
||||
let s:Vital.vital_files = function('s:vital_files')
|
||||
|
||||
function! s:import(name, ...) abort dict
|
||||
let target = {}
|
||||
let functions = []
|
||||
for a in a:000
|
||||
if type(a) == type({})
|
||||
let target = a
|
||||
elseif type(a) == type([])
|
||||
let functions = a
|
||||
endif
|
||||
unlet a
|
||||
endfor
|
||||
let module = self._import(a:name)
|
||||
if empty(functions)
|
||||
call extend(target, module, 'keep')
|
||||
else
|
||||
for f in functions
|
||||
if has_key(module, f) && !has_key(target, f)
|
||||
let target[f] = module[f]
|
||||
endif
|
||||
endfor
|
||||
endif
|
||||
return target
|
||||
endfunction
|
||||
let s:Vital.import = function('s:import')
|
||||
|
||||
function! s:load(...) abort dict
|
||||
for arg in a:000
|
||||
let [name; as] = type(arg) == type([]) ? arg[: 1] : [arg, arg]
|
||||
let target = split(join(as, ''), '\W\+')
|
||||
let dict = self
|
||||
let dict_type = type({})
|
||||
while !empty(target)
|
||||
let ns = remove(target, 0)
|
||||
if !has_key(dict, ns)
|
||||
let dict[ns] = {}
|
||||
endif
|
||||
if type(dict[ns]) == dict_type
|
||||
let dict = dict[ns]
|
||||
else
|
||||
unlet dict
|
||||
break
|
||||
endif
|
||||
endwhile
|
||||
if exists('dict')
|
||||
call extend(dict, self._import(name))
|
||||
endif
|
||||
unlet arg
|
||||
endfor
|
||||
return self
|
||||
endfunction
|
||||
let s:Vital.load = function('s:load')
|
||||
|
||||
function! s:unload() abort dict
|
||||
let s:loaded = {}
|
||||
let s:cache_sid = {}
|
||||
unlet! s:vital_files
|
||||
endfunction
|
||||
let s:Vital.unload = function('s:unload')
|
||||
|
||||
function! s:exists(name) abort dict
|
||||
if a:name !~# '\v^\u\w*%(\.\u\w*)*$'
|
||||
throw 'vital: Invalid module name: ' . a:name
|
||||
endif
|
||||
return s:_module_path(a:name) isnot# ''
|
||||
endfunction
|
||||
let s:Vital.exists = function('s:exists')
|
||||
|
||||
function! s:search(pattern) abort dict
|
||||
let paths = s:_extract_files(a:pattern, self.vital_files())
|
||||
let modules = sort(map(paths, 's:_file2module(v:val)'))
|
||||
return uniq(modules)
|
||||
endfunction
|
||||
let s:Vital.search = function('s:search')
|
||||
|
||||
function! s:plugin_name() abort dict
|
||||
return self._plugin_name
|
||||
endfunction
|
||||
let s:Vital.plugin_name = function('s:plugin_name')
|
||||
|
||||
function! s:_self_vital_files() abort
|
||||
let builtin = printf('%s/__%s__/', s:vital_base_dir, s:plugin_name)
|
||||
let installed = printf('%s/_%s/', s:vital_base_dir, s:plugin_name)
|
||||
let base = builtin . ',' . installed
|
||||
return split(globpath(base, '**/*.vim', 1), "\n")
|
||||
endfunction
|
||||
|
||||
function! s:_global_vital_files() abort
|
||||
let pattern = 'autoload/vital/__*__/**/*.vim'
|
||||
return split(globpath(&runtimepath, pattern, 1), "\n")
|
||||
endfunction
|
||||
|
||||
function! s:_extract_files(pattern, files) abort
|
||||
let tr = {'.': '/', '*': '[^/]*', '**': '.*'}
|
||||
let target = substitute(a:pattern, '\.\|\*\*\?', '\=tr[submatch(0)]', 'g')
|
||||
let regexp = printf('autoload/vital/[^/]\+/%s.vim$', target)
|
||||
return filter(a:files, 'v:val =~# regexp')
|
||||
endfunction
|
||||
|
||||
function! s:_file2module(file) abort
|
||||
let filename = fnamemodify(a:file, ':p:gs?[\\/]?/?')
|
||||
let tail = matchstr(filename, 'autoload/vital/_\w\+/\zs.*\ze\.vim$')
|
||||
return join(split(tail, '[\\/]\+'), '.')
|
||||
endfunction
|
||||
|
||||
" @param {string} name e.g. Data.List
|
||||
function! s:_import(name) abort dict
|
||||
if has_key(s:loaded, a:name)
|
||||
return copy(s:loaded[a:name])
|
||||
endif
|
||||
let module = self._get_module(a:name)
|
||||
if has_key(module, '_vital_created')
|
||||
call module._vital_created(module)
|
||||
endif
|
||||
let export_module = filter(copy(module), 'v:key =~# "^\\a"')
|
||||
" Cache module before calling module._vital_loaded() to avoid cyclic
|
||||
" dependences but remove the cache if module._vital_loaded() fails.
|
||||
" let s:loaded[a:name] = export_module
|
||||
let s:loaded[a:name] = export_module
|
||||
if has_key(module, '_vital_loaded')
|
||||
try
|
||||
call module._vital_loaded(vital#{s:plugin_name}#new())
|
||||
catch
|
||||
unlet s:loaded[a:name]
|
||||
throw 'vital: fail to call ._vital_loaded(): ' . v:exception . " from:\n" . s:_format_throwpoint(v:throwpoint)
|
||||
endtry
|
||||
endif
|
||||
return copy(s:loaded[a:name])
|
||||
endfunction
|
||||
let s:Vital._import = function('s:_import')
|
||||
|
||||
function! s:_format_throwpoint(throwpoint) abort
|
||||
let funcs = []
|
||||
let stack = matchstr(a:throwpoint, '^function \zs.*, .\{-} \d\+$')
|
||||
for line in split(stack, '\.\.')
|
||||
let m = matchlist(line, '^\(.\+\)\%(\[\(\d\+\)\]\|, .\{-} \(\d\+\)\)$')
|
||||
if !empty(m)
|
||||
let [name, lnum, lnum2] = m[1:3]
|
||||
if empty(lnum)
|
||||
let lnum = lnum2
|
||||
endif
|
||||
let info = s:_get_func_info(name)
|
||||
if !empty(info)
|
||||
let attrs = empty(info.attrs) ? '' : join([''] + info.attrs)
|
||||
let flnum = info.lnum == 0 ? '' : printf(' Line:%d', info.lnum + lnum)
|
||||
call add(funcs, printf('function %s(...)%s Line:%d (%s%s)',
|
||||
\ info.funcname, attrs, lnum, info.filename, flnum))
|
||||
continue
|
||||
endif
|
||||
endif
|
||||
" fallback when function information cannot be detected
|
||||
call add(funcs, line)
|
||||
endfor
|
||||
return join(funcs, "\n")
|
||||
endfunction
|
||||
|
||||
function! s:_get_func_info(name) abort
|
||||
let name = a:name
|
||||
if a:name =~# '^\d\+$' " is anonymous-function
|
||||
let name = printf('{%s}', a:name)
|
||||
elseif a:name =~# '^<lambda>\d\+$' " is lambda-function
|
||||
let name = printf("{'%s'}", a:name)
|
||||
endif
|
||||
if !exists('*' . name)
|
||||
return {}
|
||||
endif
|
||||
let body = execute(printf('verbose function %s', name))
|
||||
let lines = split(body, "\n")
|
||||
let signature = matchstr(lines[0], '^\s*\zs.*')
|
||||
let [_, file, lnum; __] = matchlist(lines[1],
|
||||
\ '^\t\%(Last set from\|.\{-}:\)\s*\zs\(.\{-}\)\%( \S\+ \(\d\+\)\)\?$')
|
||||
return {
|
||||
\ 'filename': substitute(file, '[/\\]\+', '/', 'g'),
|
||||
\ 'lnum': 0 + lnum,
|
||||
\ 'funcname': a:name,
|
||||
\ 'arguments': split(matchstr(signature, '(\zs.*\ze)'), '\s*,\s*'),
|
||||
\ 'attrs': filter(['dict', 'abort', 'range', 'closure'], 'signature =~# (").*" . v:val)'),
|
||||
\ }
|
||||
endfunction
|
||||
|
||||
" s:_get_module() returns module object wihch has all script local functions.
|
||||
function! s:_get_module(name) abort dict
|
||||
let funcname = s:_import_func_name(self.plugin_name(), a:name)
|
||||
try
|
||||
return call(funcname, [])
|
||||
catch /^Vim\%((\a\+)\)\?:E117:/
|
||||
return s:_get_builtin_module(a:name)
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
function! s:_get_builtin_module(name) abort
|
||||
return s:sid2sfuncs(s:_module_sid(a:name))
|
||||
endfunction
|
||||
|
||||
if s:is_vital_vim
|
||||
" For vital.vim, we can use s:_get_builtin_module directly
|
||||
let s:Vital._get_module = function('s:_get_builtin_module')
|
||||
else
|
||||
let s:Vital._get_module = function('s:_get_module')
|
||||
endif
|
||||
|
||||
function! s:_import_func_name(plugin_name, module_name) abort
|
||||
return printf('vital#_%s#%s#import', a:plugin_name, s:_dot_to_sharp(a:module_name))
|
||||
endfunction
|
||||
|
||||
function! s:_module_sid(name) abort
|
||||
let path = s:_module_path(a:name)
|
||||
if !filereadable(path)
|
||||
throw 'vital: module not found: ' . a:name
|
||||
endif
|
||||
let vital_dir = s:is_vital_vim ? '__\w\+__' : printf('_\{1,2}%s\%%(__\)\?', s:plugin_name)
|
||||
let base = join([vital_dir, ''], '[/\\]\+')
|
||||
let p = base . substitute('' . a:name, '\.', '[/\\\\]\\+', 'g')
|
||||
let sid = s:_sid(path, p)
|
||||
if !sid
|
||||
call s:_source(path)
|
||||
let sid = s:_sid(path, p)
|
||||
if !sid
|
||||
throw printf('vital: cannot get <SID> from path: %s', path)
|
||||
endif
|
||||
endif
|
||||
return sid
|
||||
endfunction
|
||||
|
||||
function! s:_module_path(name) abort
|
||||
return get(s:_extract_files(a:name, s:vital_files()), 0, '')
|
||||
endfunction
|
||||
|
||||
function! s:_module_sid_base_dir() abort
|
||||
return s:is_vital_vim ? &rtp : s:project_root
|
||||
endfunction
|
||||
|
||||
function! s:_dot_to_sharp(name) abort
|
||||
return substitute(a:name, '\.', '#', 'g')
|
||||
endfunction
|
||||
|
||||
function! s:_source(path) abort
|
||||
execute 'source' fnameescape(a:path)
|
||||
endfunction
|
||||
|
||||
" @vimlint(EVL102, 1, l:_)
|
||||
" @vimlint(EVL102, 1, l:__)
|
||||
function! s:_sid(path, filter_pattern) abort
|
||||
let unified_path = s:_unify_path(a:path)
|
||||
if has_key(s:cache_sid, unified_path)
|
||||
return s:cache_sid[unified_path]
|
||||
endif
|
||||
for line in filter(split(execute(':scriptnames'), "\n"), 'v:val =~# a:filter_pattern')
|
||||
let [_, sid, path; __] = matchlist(line, '^\s*\(\d\+\):\s\+\(.\+\)\s*$')
|
||||
if s:_unify_path(path) is# unified_path
|
||||
let s:cache_sid[unified_path] = sid
|
||||
return s:cache_sid[unified_path]
|
||||
endif
|
||||
endfor
|
||||
return 0
|
||||
endfunction
|
||||
|
||||
if filereadable(expand('<sfile>:r') . '.VIM') " is case-insensitive or not
|
||||
let s:_unify_path_cache = {}
|
||||
" resolve() is slow, so we cache results.
|
||||
" Note: On windows, vim can't expand path names from 8.3 formats.
|
||||
" So if getting full path via <sfile> and $HOME was set as 8.3 format,
|
||||
" vital load duplicated scripts. Below's :~ avoid this issue.
|
||||
function! s:_unify_path(path) abort
|
||||
if has_key(s:_unify_path_cache, a:path)
|
||||
return s:_unify_path_cache[a:path]
|
||||
endif
|
||||
let value = tolower(fnamemodify(resolve(fnamemodify(
|
||||
\ a:path, ':p')), ':~:gs?[\\/]?/?'))
|
||||
let s:_unify_path_cache[a:path] = value
|
||||
return value
|
||||
endfunction
|
||||
else
|
||||
function! s:_unify_path(path) abort
|
||||
return resolve(fnamemodify(a:path, ':p:gs?[\\/]?/?'))
|
||||
endfunction
|
||||
endif
|
||||
|
||||
" copied and modified from Vim.ScriptLocal
|
||||
let s:SNR = join(map(range(len("\<SNR>")), '"[\\x" . printf("%0x", char2nr("\<SNR>"[v:val])) . "]"'), '')
|
||||
function! s:sid2sfuncs(sid) abort
|
||||
let fs = split(execute(printf(':function /^%s%s_', s:SNR, a:sid)), "\n")
|
||||
let r = {}
|
||||
let pattern = printf('\m^function\s<SNR>%d_\zs\w\{-}\ze(', a:sid)
|
||||
for fname in map(fs, 'matchstr(v:val, pattern)')
|
||||
let r[fname] = function(s:_sfuncname(a:sid, fname))
|
||||
endfor
|
||||
return r
|
||||
endfunction
|
||||
|
||||
"" Return funcname of script local functions with SID
|
||||
function! s:_sfuncname(sid, funcname) abort
|
||||
return printf('<SNR>%s_%s', a:sid, a:funcname)
|
||||
endfunction
|
5
autoload/vital/cmp.vital
Normal file
5
autoload/vital/cmp.vital
Normal file
|
@ -0,0 +1,5 @@
|
|||
cmp
|
||||
5828301d6bae0858e9ea21012913544f5ef8e375
|
||||
|
||||
VS.LSP.CompletionItem
|
||||
VS.LSP.Position
|
7
init.sh
Executable file
7
init.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
|
||||
rm $DIR/.git/hooks/*
|
||||
cp $DIR/.githooks/* $DIR/.git/hooks/
|
||||
chmod 755 $DIR/.git/hooks/*
|
||||
|
||||
|
34
lua/cmp/autocmd.lua
Normal file
34
lua/cmp/autocmd.lua
Normal file
|
@ -0,0 +1,34 @@
|
|||
local debug = require('cmp.utils.debug')
|
||||
|
||||
local autocmd = {}
|
||||
|
||||
autocmd.events = {}
|
||||
|
||||
---Subscribe autocmd
|
||||
---@param event string
|
||||
---@param callback function
|
||||
---@return function
|
||||
autocmd.subscribe = function(event, callback)
|
||||
autocmd.events[event] = autocmd.events[event] or {}
|
||||
table.insert(autocmd.events[event], callback)
|
||||
return function()
|
||||
for i, callback_ in ipairs(autocmd.events[event]) do
|
||||
if callback_ == callback then
|
||||
table.remove(autocmd.events[event], i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Emit autocmd
|
||||
---@param event string
|
||||
autocmd.emit = function(event)
|
||||
debug.log(string.format('>>> %s', event))
|
||||
autocmd.events[event] = autocmd.events[event] or {}
|
||||
for _, callback in ipairs(autocmd.events[event]) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
return autocmd
|
63
lua/cmp/config.lua
Normal file
63
lua/cmp/config.lua
Normal file
|
@ -0,0 +1,63 @@
|
|||
local cache = require('cmp.utils.cache')
|
||||
local misc = require('cmp.utils.misc')
|
||||
|
||||
---@class cmp.Config
|
||||
---@field public g cmp.ConfigSchema
|
||||
local config = {}
|
||||
|
||||
---@type cmp.Cache
|
||||
config.cache = cache.new()
|
||||
|
||||
---@type cmp.ConfigSchema
|
||||
config.global = require('cmp.config.default')()
|
||||
|
||||
---@type table<number, cmp.ConfigSchema>
|
||||
config.buffers = {}
|
||||
|
||||
---Set configuration for global.
|
||||
---@param c cmp.ConfigSchema
|
||||
config.set_global = function(c)
|
||||
config.global = 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
|
||||
config.set_buffer = function(c, bufnr)
|
||||
config.buffers[bufnr] = c
|
||||
config.buffers[bufnr].revision = config.buffers[bufnr].revision or 1
|
||||
config.buffers[bufnr].revision = config.buffers[bufnr].revision + 1
|
||||
end
|
||||
|
||||
---@return cmp.ConfigSchema
|
||||
config.get = function()
|
||||
local global = config.global
|
||||
local buffer = config.buffers[vim.api.nvim_get_current_buf()] or { revision = 1 }
|
||||
return config.cache:ensure({ 'get', global.revision or 0, buffer.revision or 0 }, function()
|
||||
return misc.merge(buffer, global)
|
||||
end)
|
||||
end
|
||||
|
||||
---Return source option
|
||||
---@param name string
|
||||
---@return table
|
||||
config.get_source_option = function(name)
|
||||
local global = config.global
|
||||
local buffer = config.buffers[vim.api.nvim_get_current_buf()] or { revision = 1 }
|
||||
return config.cache:ensure({ 'get_source_config', global.revision or 0, buffer.revision or 0, name }, function()
|
||||
local c = config.get()
|
||||
for _, s in ipairs(c.sources) do
|
||||
if s.name == name then
|
||||
if type(s.opts) == 'table' then
|
||||
return s.opts
|
||||
end
|
||||
return {}
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end)
|
||||
end
|
||||
|
||||
return config
|
88
lua/cmp/config/compare.lua
Normal file
88
lua/cmp/config/compare.lua
Normal file
|
@ -0,0 +1,88 @@
|
|||
local types = require'cmp.types'
|
||||
local misc = require 'cmp.utils.misc'
|
||||
|
||||
local compare = {}
|
||||
|
||||
-- offset
|
||||
compare.offset = function(entry1, entry2)
|
||||
local diff = entry1:get_offset() - entry2:get_offset()
|
||||
if diff < 0 then
|
||||
return true
|
||||
elseif diff > 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- exact
|
||||
compare.exact = function(entry1, entry2)
|
||||
if entry1.exact ~= entry2.exact then
|
||||
return entry1.exact
|
||||
end
|
||||
end
|
||||
|
||||
-- score
|
||||
compare.score = function(entry1, entry2)
|
||||
local diff = entry2.score - entry1.score
|
||||
if diff < 0 then
|
||||
return true
|
||||
elseif diff > 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- kind
|
||||
compare.kind = function(entry1, entry2)
|
||||
local kind1 = entry1:get_kind()
|
||||
kind1 = kind1 == types.lsp.CompletionItemKind.Text and 100 or kind1
|
||||
local kind2 = entry2:get_kind()
|
||||
kind2 = kind2 == types.lsp.CompletionItemKind.Text and 100 or kind2
|
||||
if kind1 ~= kind2 then
|
||||
if kind1 == types.lsp.CompletionItemKind.Snippet then
|
||||
return true
|
||||
end
|
||||
if kind2 == types.lsp.CompletionItemKind.Snippet then
|
||||
return false
|
||||
end
|
||||
local diff = kind1 - kind2
|
||||
if diff < 0 then
|
||||
return true
|
||||
elseif diff > 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- sortText
|
||||
compare.sort_text = function(entry1, entry2)
|
||||
if misc.safe(entry1.completion_item.sortText) and misc.safe(entry2.completion_item.sortText) then
|
||||
local diff = vim.stricmp(entry1.completion_item.sortText, entry2.completion_item.sortText)
|
||||
if diff < 0 then
|
||||
return true
|
||||
elseif diff > 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- length
|
||||
compare.length = function(entry1, entry2)
|
||||
local diff = #entry1.completion_item.label - #entry2.completion_item.label
|
||||
if diff < 0 then
|
||||
return true
|
||||
elseif diff > 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- order
|
||||
compare.order = function(entry1, entry2)
|
||||
local diff = entry1.id - entry2.id
|
||||
if diff < 0 then
|
||||
return true
|
||||
elseif diff > 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return compare
|
||||
|
119
lua/cmp/config/default.lua
Normal file
119
lua/cmp/config/default.lua
Normal file
|
@ -0,0 +1,119 @@
|
|||
local str = require('cmp.utils.str')
|
||||
local misc = require('cmp.utils.misc')
|
||||
local compare = require('cmp.config.compare')
|
||||
local types = require('cmp.types')
|
||||
|
||||
local WIDE_HEIGHT = 40
|
||||
|
||||
---@return cmp.ConfigSchema
|
||||
return function()
|
||||
return {
|
||||
completion = {
|
||||
autocomplete = {
|
||||
types.cmp.TriggerEvent.InsertEnter,
|
||||
types.cmp.TriggerEvent.TextChanged,
|
||||
},
|
||||
completeopt = 'menu,menuone,noselect',
|
||||
keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%(-\w*\)*\)]],
|
||||
keyword_length = 1,
|
||||
},
|
||||
|
||||
snippet = {
|
||||
expand = function()
|
||||
error('snippet engine does not configured.')
|
||||
end,
|
||||
},
|
||||
|
||||
documentation = {
|
||||
border = { '', '', '', ' ', '', '', '', ' ' },
|
||||
winhighlight = 'NormalFloat:CmpDocumentation,FloatBorder:CmpDocumentationBorder',
|
||||
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.Replace,
|
||||
mapping = {
|
||||
['<CR>'] = {
|
||||
behavior = types.cmp.ConfirmBehavior.Replace,
|
||||
select = true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
sorting = {
|
||||
sort = function(entries)
|
||||
table.sort(entries, function(entry1, entry2)
|
||||
for _, fn in ipairs({
|
||||
compare.offset,
|
||||
compare.exact,
|
||||
compare.score,
|
||||
compare.kind,
|
||||
compare.sort_text,
|
||||
compare.length,
|
||||
compare.order,
|
||||
}) do
|
||||
local diff = fn(entry1, entry2)
|
||||
if diff ~= nil then
|
||||
return diff
|
||||
end
|
||||
end
|
||||
return true
|
||||
end)
|
||||
return entries
|
||||
end
|
||||
},
|
||||
|
||||
formatting = {
|
||||
format = function(e, suggest_offset)
|
||||
local item = e:get_completion_item()
|
||||
local word = e:get_word()
|
||||
local abbr = str.trim(item.label)
|
||||
|
||||
-- ~ indicator
|
||||
if #(misc.safe(item.additionalTextEdits) or {}) > 0 then
|
||||
abbr = abbr .. '~'
|
||||
elseif item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
|
||||
local insert_text = e:get_insert_text()
|
||||
if word ~= insert_text then
|
||||
abbr = abbr .. '~'
|
||||
end
|
||||
end
|
||||
|
||||
-- deprecated
|
||||
if item.deprecated or vim.tbl_contains(item.tags or {}, types.lsp.CompletionItemTag.Deprecated) then
|
||||
abbr = str.strikethrough(abbr)
|
||||
end
|
||||
|
||||
-- append delta text
|
||||
if suggest_offset < e:get_offset() then
|
||||
word = string.sub(e.context.cursor_before_line, suggest_offset, e:get_offset() - 1) .. word
|
||||
end
|
||||
|
||||
-- labelDetails.
|
||||
local menu = nil
|
||||
if misc.safe(item.labelDetails) then
|
||||
menu = ''
|
||||
if misc.safe(item.labelDetails.parameters) then
|
||||
menu = menu .. item.labelDetails.parameters
|
||||
end
|
||||
if misc.safe(item.labelDetails.type) then
|
||||
menu = menu .. item.labelDetails.type
|
||||
end
|
||||
if misc.safe(item.labelDetails.qualifier) then
|
||||
menu = menu .. item.labelDetails.qualifier
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
word = word,
|
||||
abbr = abbr,
|
||||
kind = types.lsp.CompletionItemKind[e:get_kind()] or types.lsp.CompletionItemKind[1],
|
||||
menu = menu,
|
||||
}
|
||||
end
|
||||
},
|
||||
|
||||
sources = {},
|
||||
}
|
||||
end
|
142
lua/cmp/context.lua
Normal file
142
lua/cmp/context.lua
Normal file
|
@ -0,0 +1,142 @@
|
|||
local misc = require('cmp.utils.misc')
|
||||
local pattern = require('cmp.utils.pattern')
|
||||
local types = require('cmp.types')
|
||||
local cache = require('cmp.utils.cache')
|
||||
|
||||
---@class cmp.Context
|
||||
---@field public id string
|
||||
---@field public cache cmp.Cache
|
||||
---@field public prev_context cmp.Context
|
||||
---@field public option cmp.ContextOption
|
||||
---@field public pumvisible boolean
|
||||
---@field public pumselect boolean
|
||||
---@field public filetype string
|
||||
---@field public time number
|
||||
---@field public mode string
|
||||
---@field public bufnr number
|
||||
---@field public cursor vim.Position
|
||||
---@field public cursor_line string
|
||||
---@field public cursor_after_line string
|
||||
---@field public cursor_before_line string
|
||||
---@field public before_char string
|
||||
local context = {}
|
||||
|
||||
---Create new empty context
|
||||
---@return cmp.Context
|
||||
context.empty = function()
|
||||
local ctx = context.new({}) -- dirty hack to prevent recursive call `context.empty`.
|
||||
ctx.bufnr = -1
|
||||
ctx.input = ''
|
||||
ctx.cursor = {}
|
||||
ctx.cursor.row = -1
|
||||
ctx.cursor.col = -1
|
||||
return ctx
|
||||
end
|
||||
|
||||
---Create new context
|
||||
---@param prev_context cmp.Context
|
||||
---@param option cmp.ContextOption
|
||||
---@return cmp.Context
|
||||
context.new = function(prev_context, option)
|
||||
option = option or {}
|
||||
|
||||
local self = setmetatable({}, { __index = context })
|
||||
local completeinfo = vim.fn.complete_info({ 'selected', 'mode', 'pum_visible' })
|
||||
self.id = misc.id('context')
|
||||
self.cache = cache.new()
|
||||
self.prev_context = prev_context or context.empty()
|
||||
self.option = option or { reason = types.cmp.ContextReason.None }
|
||||
self.pumvisible = completeinfo.pum_visible ~= 0
|
||||
self.pumselect = completeinfo.selected ~= -1
|
||||
self.filetype = vim.api.nvim_buf_get_option(0, 'filetype')
|
||||
self.time = vim.loop.now()
|
||||
self.mode = vim.api.nvim_get_mode().mode
|
||||
self.bufnr = vim.api.nvim_get_current_buf()
|
||||
self.cursor = {}
|
||||
self.cursor.row = vim.api.nvim_win_get_cursor(0)[1]
|
||||
self.cursor.col = vim.api.nvim_win_get_cursor(0)[2] + 1
|
||||
self.cursor_line = vim.api.nvim_get_current_line()
|
||||
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.before_char = string.sub(self.cursor_line, self.cursor.col - 1, self.cursor.col - 1)
|
||||
return self
|
||||
end
|
||||
|
||||
---Return context creation reason.
|
||||
---@return cmp.ContextReason
|
||||
context.get_reason = function(self)
|
||||
return self.option.reason
|
||||
end
|
||||
|
||||
---Get keyword pattern offset
|
||||
---@return number|nil
|
||||
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 .. '$', self.cursor_before_line) or self.cursor.col
|
||||
end)
|
||||
end
|
||||
|
||||
---if cursor moves from left to right.
|
||||
---@param self cmp.Context
|
||||
context.is_forwarding = function(self)
|
||||
local prev = self.prev_context
|
||||
local curr = self
|
||||
|
||||
return prev.bufnr == curr.bufnr and prev.cursor.row == curr.cursor.row and prev.cursor.col < curr.cursor.col
|
||||
end
|
||||
|
||||
---Return if this context is continueing previous context.
|
||||
context.continue = function(self, offset)
|
||||
local prev = self.prev_context
|
||||
local curr = self
|
||||
|
||||
if curr.bufnr ~= prev.bufnr then
|
||||
return false
|
||||
end
|
||||
if curr.cursor.row ~= prev.cursor.row then
|
||||
return false
|
||||
end
|
||||
if curr.cursor.col < offset then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---Return if this context is changed from previous context or not.
|
||||
---@return boolean
|
||||
context.changed = function(self, ctx)
|
||||
local curr = self
|
||||
|
||||
if self.pumvisible then
|
||||
local completed_item = vim.v.completed_item or {}
|
||||
if completed_item.word then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
if curr.bufnr ~= ctx.bufnr then
|
||||
return true
|
||||
end
|
||||
if curr.cursor.row ~= ctx.cursor.row then
|
||||
return true
|
||||
end
|
||||
if curr.cursor.col ~= ctx.cursor.col then
|
||||
return true
|
||||
end
|
||||
if curr:get_reason() == types.cmp.ContextReason.Manual then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---Shallow clone
|
||||
context.clone = function(self)
|
||||
local cloned = {}
|
||||
for k, v in pairs(self) do
|
||||
cloned[k] = v
|
||||
end
|
||||
return cloned
|
||||
end
|
||||
|
||||
return context
|
31
lua/cmp/context_spec.lua
Normal file
31
lua/cmp/context_spec.lua
Normal file
|
@ -0,0 +1,31 @@
|
|||
local spec = require('cmp.utils.spec')
|
||||
|
||||
local context = require('cmp.context')
|
||||
|
||||
describe('context', function()
|
||||
before_each(spec.before)
|
||||
|
||||
describe('new', function()
|
||||
it('middle of text', function()
|
||||
vim.fn.setline('1', 'function! s:name() abort')
|
||||
vim.bo.filetype = 'vim'
|
||||
vim.fn.execute('normal! fm')
|
||||
local ctx = context.new()
|
||||
assert.are.equal(ctx.filetype, 'vim')
|
||||
assert.are.equal(ctx.cursor.row, 1)
|
||||
assert.are.equal(ctx.cursor.col, 15)
|
||||
assert.are.equal(ctx.cursor_line, 'function! s:name() abort')
|
||||
end)
|
||||
|
||||
it('tab indent', function()
|
||||
vim.fn.setline('1', '\t\tab')
|
||||
vim.bo.filetype = 'vim'
|
||||
vim.fn.execute('normal! fb')
|
||||
local ctx = context.new()
|
||||
assert.are.equal(ctx.filetype, 'vim')
|
||||
assert.are.equal(ctx.cursor.row, 1)
|
||||
assert.are.equal(ctx.cursor.col, 4)
|
||||
assert.are.equal(ctx.cursor_line, '\t\tab')
|
||||
end)
|
||||
end)
|
||||
end)
|
255
lua/cmp/core.lua
Normal file
255
lua/cmp/core.lua
Normal file
|
@ -0,0 +1,255 @@
|
|||
local debug = require('cmp.utils.debug')
|
||||
local char = require('cmp.utils.char')
|
||||
local async = require('cmp.utils.async')
|
||||
local keymap = require('cmp.utils.keymap')
|
||||
local context = require('cmp.context')
|
||||
local source = require('cmp.source')
|
||||
local menu = require('cmp.menu')
|
||||
local misc = require('cmp.utils.misc')
|
||||
local config = require('cmp.config')
|
||||
local types = require('cmp.types')
|
||||
local patch = require('cmp.utils.patch')
|
||||
|
||||
local core = {}
|
||||
|
||||
core.SOURCE_TIMEOUT = 500
|
||||
|
||||
---@type cmp.Menu
|
||||
core.menu = menu.new()
|
||||
|
||||
---@type table<number, cmp.Source>
|
||||
core.sources = {}
|
||||
|
||||
---@type cmp.Context
|
||||
core.context = context.new()
|
||||
|
||||
---Register source
|
||||
---@param s cmp.Source
|
||||
core.register_source = function(s)
|
||||
core.sources[s.id] = s
|
||||
end
|
||||
|
||||
---Unregister source
|
||||
---@param source_id string
|
||||
core.unregister_source = function(source_id)
|
||||
core.sources[source_id] = nil
|
||||
end
|
||||
|
||||
---Get new context
|
||||
---@param option cmp.ContextOption
|
||||
---@return cmp.Context
|
||||
core.get_context = function(option)
|
||||
local prev = core.context:clone()
|
||||
prev.prev_context = nil
|
||||
core.context = context.new(prev, option)
|
||||
return core.context
|
||||
end
|
||||
|
||||
---Get sources that sorted by priority
|
||||
---@param statuses cmp.SourceStatus[]
|
||||
---@return cmp.Source[]
|
||||
core.get_sources = function(statuses)
|
||||
local sources = {}
|
||||
for _, c in pairs(config.get().sources) do
|
||||
for _, s in pairs(core.sources) do
|
||||
if c.name == s.name then
|
||||
if not statuses or vim.tbl_contains(statuses, s.status) then
|
||||
table.insert(sources, s)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return sources
|
||||
end
|
||||
|
||||
---Keypress handler
|
||||
core.on_keymap = function(keys, fallback)
|
||||
-- Confirm character
|
||||
if config.get().confirmation.mapping[keys] then
|
||||
local c = config.get().confirmation.mapping[keys]
|
||||
local e = core.menu:get_selected_entry() or (c.select and core.menu:get_first_entry())
|
||||
if not e then
|
||||
return fallback()
|
||||
end
|
||||
return core.confirm(e, {
|
||||
behavior = c.behavior,
|
||||
})
|
||||
end
|
||||
|
||||
--Commit character. NOTE: This has a lot of cmp specific implementation to make more user-friendly.
|
||||
local chars = keymap.t(keys)
|
||||
local e = core.menu:get_selected_entry()
|
||||
if e and vim.tbl_contains(e:get_commit_characters(), chars) then
|
||||
local is_printable = char.is_printable(string.byte(chars, 1))
|
||||
return core.confirm(e, {
|
||||
behavior = is_printable and 'insert' or 'replace',
|
||||
}, function()
|
||||
local ctx = core.get_context()
|
||||
local word = e:get_word()
|
||||
if string.sub(ctx.cursor_before_line, -#word, ctx.cursor.col - 1) == word and is_printable then
|
||||
fallback()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
fallback()
|
||||
end
|
||||
|
||||
---Prepare completion
|
||||
core.prepare = function()
|
||||
for keys in pairs(config.get().confirmation.mapping) do
|
||||
keymap.listen(keys, core.on_keymap)
|
||||
end
|
||||
end
|
||||
|
||||
---Check auto-completion
|
||||
core.autocomplete = function(event)
|
||||
local ctx = core.get_context({ reason = types.cmp.ContextReason.Auto })
|
||||
|
||||
-- Skip autocompletion when the item is selected manually.
|
||||
if ctx.pumvisible and not vim.tbl_isempty(vim.v.completed_item) then
|
||||
return
|
||||
end
|
||||
|
||||
debug.log(('ctx: `%s`'):format(ctx.cursor_before_line))
|
||||
if ctx:is_forwarding() then
|
||||
debug.log('changed')
|
||||
core.menu:restore(ctx)
|
||||
|
||||
if vim.tbl_contains(config.get().completion.autocomplete, event) then
|
||||
core.complete(ctx)
|
||||
else
|
||||
core.filter.timeout = 50
|
||||
core.filter()
|
||||
end
|
||||
else
|
||||
debug.log('unchanged')
|
||||
end
|
||||
end
|
||||
|
||||
---Invoke completion
|
||||
---@param ctx cmp.Context
|
||||
core.complete = function(ctx)
|
||||
for _, s in ipairs(core.get_sources({ source.SourceStatus.WAITING, source.SourceStatus.COMPLETED })) do
|
||||
s:complete(ctx, function()
|
||||
local new = context.new(ctx)
|
||||
if new:changed(new.prev_context) then
|
||||
core.complete(new)
|
||||
else
|
||||
core.filter.timeout = 50
|
||||
core.filter()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
core.filter.timeout = ctx.pumvisible and 50 or 0
|
||||
core.filter()
|
||||
end
|
||||
|
||||
---Update completion menu
|
||||
core.filter = async.throttle(function()
|
||||
local ctx = core.get_context()
|
||||
|
||||
-- To wait for processing source for that's timeout.
|
||||
for _, s in ipairs(core.get_sources({ source.SourceStatus.FETCHING })) do
|
||||
local time = core.SOURCE_TIMEOUT - s:get_fetching_time()
|
||||
if time > 0 then
|
||||
core.filter.stop()
|
||||
core.filter.timeout = time + 1
|
||||
core.filter()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
core.menu:update(ctx, core.get_sources())
|
||||
end, 50)
|
||||
|
||||
---Confirm completion.
|
||||
---@param e cmp.Entry
|
||||
---@param option cmp.ConfirmOption
|
||||
---@param callback function
|
||||
core.confirm = vim.schedule_wrap(function(e, option, callback)
|
||||
if not (e and not e.confirmed) then
|
||||
return
|
||||
end
|
||||
e.confirmed = true
|
||||
|
||||
debug.log('entry.confirm', e:get_completion_item())
|
||||
|
||||
--@see https://github.com/microsoft/vscode/blob/main/src/vs/editor/contrib/suggest/suggestController.ts#L334
|
||||
local pre = context.new()
|
||||
if #(misc.safe(e:get_completion_item().additionalTextEdits) or {}) == 0 then
|
||||
local new = context.new(pre)
|
||||
e:resolve(function()
|
||||
local text_edits = misc.safe(e:get_completion_item().additionalTextEdits) or {}
|
||||
if #text_edits == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local has_cursor_line_text_edit = (function()
|
||||
local minrow = math.min(pre.cursor.row, new.cursor.row)
|
||||
local maxrow = math.max(pre.cursor.row, new.cursor.row)
|
||||
for _, text_edit in ipairs(text_edits) do
|
||||
local srow = text_edit.range.start.line + 1
|
||||
local erow = text_edit.range['end'].line + 1
|
||||
if srow <= minrow and maxrow <= erow then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end)()
|
||||
if has_cursor_line_text_edit then
|
||||
return
|
||||
end
|
||||
|
||||
vim.fn['cmp#apply_text_edits'](new.bufnr, text_edits)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Prepare completion item for confirmation
|
||||
local completion_item = misc.copy(e:get_completion_item())
|
||||
if not misc.safe(completion_item.textEdit) then
|
||||
completion_item.textEdit = {}
|
||||
completion_item.textEdit.newText = misc.safe(completion_item.insertText) or completion_item.label
|
||||
end
|
||||
local behavior = option.behavior or config.get().confirmation.default_behavior
|
||||
if behavior == types.cmp.ConfirmBehavior.Replace then
|
||||
completion_item.textEdit.range = e:get_replace_range()
|
||||
else
|
||||
completion_item.textEdit.range = e:get_insert_range()
|
||||
end
|
||||
|
||||
-- First, emulates vim's `<C-y>` behavior and then confirms LSP functionalities.
|
||||
patch.apply(
|
||||
pre,
|
||||
completion_item.textEdit.range,
|
||||
e:get_word(),
|
||||
vim.schedule_wrap(function()
|
||||
vim.fn['cmp#confirm']({
|
||||
request_offset = e.context.cursor.col,
|
||||
suggest_offset = e:get_offset(),
|
||||
completion_item = completion_item,
|
||||
})
|
||||
|
||||
-- execute
|
||||
e:execute(function()
|
||||
core.reset()
|
||||
if callback then
|
||||
callback()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
)
|
||||
end)
|
||||
|
||||
---Reset current completion state
|
||||
core.reset = function()
|
||||
for _, s in pairs(core.sources) do
|
||||
s:reset()
|
||||
end
|
||||
core.menu:reset()
|
||||
|
||||
core.get_context() -- To prevent new event
|
||||
end
|
||||
|
||||
return core
|
321
lua/cmp/entry.lua
Normal file
321
lua/cmp/entry.lua
Normal file
|
@ -0,0 +1,321 @@
|
|||
local cache = require('cmp.utils.cache')
|
||||
local char = require('cmp.utils.char')
|
||||
local misc = require('cmp.utils.misc')
|
||||
local str = require('cmp.utils.str')
|
||||
local config = require('cmp.config')
|
||||
local types = require('cmp.types')
|
||||
|
||||
---@class cmp.Entry
|
||||
---@field public id number
|
||||
---@field public cache cmp.Cache
|
||||
---@field public score number
|
||||
---@field public exact boolean
|
||||
---@field public context cmp.Context
|
||||
---@field public source cmp.Source
|
||||
---@field public source_offset number
|
||||
---@field public source_insert_range lsp.Range
|
||||
---@field public source_replace_range lsp.Range
|
||||
---@field public completion_item lsp.CompletionItem
|
||||
---@field public resolved_completion_item lsp.CompletionItem|nil
|
||||
---@field public resolved_callbacks fun()[]
|
||||
---@field public resolving boolean
|
||||
---@field public confirmed boolean
|
||||
local entry = {}
|
||||
|
||||
---Create new entry
|
||||
---@param ctx cmp.Context
|
||||
---@param source cmp.Source
|
||||
---@param completion_item lsp.CompletionItem
|
||||
---@return cmp.Entry
|
||||
entry.new = function(ctx, source, completion_item)
|
||||
local self = setmetatable({}, { __index = entry })
|
||||
self.id = misc.id('entry')
|
||||
self.cache = cache.new()
|
||||
self.score = 0
|
||||
self.context = ctx
|
||||
self.source = source
|
||||
self.source_offset = source.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.resolved_completion_item = nil
|
||||
self.resolved_callbacks = {}
|
||||
self.resolving = false
|
||||
self.confirmed = false
|
||||
return self
|
||||
end
|
||||
|
||||
---Make offset value
|
||||
---@return number
|
||||
entry.get_offset = function(self)
|
||||
return self.cache:ensure('get_offset', function()
|
||||
local offset = self.source_offset
|
||||
if misc.safe(self.completion_item.textEdit) then
|
||||
local range = misc.safe(self.completion_item.textEdit.insert) or misc.safe(self.completion_item.textEdit.range)
|
||||
if range then
|
||||
local c = vim.str_byteindex(self.context.cursor_line, range.start.character) + 1
|
||||
for idx = c, self.source_offset do
|
||||
if not char.is_white(string.byte(self.context.cursor_line, idx)) then
|
||||
offset = math.min(offset, idx)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
-- NOTE
|
||||
-- The VSCode does not implement this but it's useful if the server does not care about word patterns.
|
||||
-- We should care about this performance.
|
||||
local word = self:get_word()
|
||||
for idx = self.source_offset - 1, self.source_offset - #word, -1 do
|
||||
if char.is_semantic_index(self.context.cursor_line, idx) then
|
||||
local c = string.byte(self.context.cursor_line, idx)
|
||||
if char.is_white(c) then
|
||||
break
|
||||
end
|
||||
local match = true
|
||||
for i = 1, self.source_offset - idx do
|
||||
local c1 = string.byte(word, i)
|
||||
local c2 = string.byte(self.context.cursor_line, idx + i - 1)
|
||||
if not c1 or not c2 or c1 ~= c2 then
|
||||
match = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if match then
|
||||
offset = math.min(offset, idx)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return offset
|
||||
end)
|
||||
end
|
||||
|
||||
---Create word for vim.CompletedItem
|
||||
---@return string
|
||||
entry.get_word = function(self)
|
||||
return self.cache:ensure('get_word', function()
|
||||
--NOTE: This is nvim-cmp specific implementation.
|
||||
if misc.safe(self.completion_item.word) then
|
||||
return self.completion_item.word
|
||||
end
|
||||
|
||||
local word
|
||||
if misc.safe(self.completion_item.textEdit) then
|
||||
word = str.trim(self.completion_item.textEdit.newText)
|
||||
local _, after = self:get_overwrite()
|
||||
if 0 < after or self.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
|
||||
word = str.get_word(word, string.byte(self.context.cursor_after_line, 1))
|
||||
end
|
||||
elseif misc.safe(self.completion_item.insertText) then
|
||||
word = str.trim(self.completion_item.insertText)
|
||||
if self.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
|
||||
word = str.get_word(word)
|
||||
end
|
||||
else
|
||||
word = str.trim(self.completion_item.label)
|
||||
end
|
||||
return word
|
||||
end)
|
||||
end
|
||||
|
||||
---Get overwrite information
|
||||
---@return number, number
|
||||
entry.get_overwrite = function(self)
|
||||
return self.cache:ensure('get_overwrite', function()
|
||||
if misc.safe(self.completion_item.textEdit) then
|
||||
local r = misc.safe(self.completion_item.textEdit.insert) or misc.safe(self.completion_item.textEdit.range)
|
||||
local s = vim.str_byteindex(self.context.cursor_line, r.start.character) + 1
|
||||
local e = vim.str_byteindex(self.context.cursor_line, r['end'].character) + 1
|
||||
local before = self.context.cursor.col - s
|
||||
local after = e - self.context.cursor.col
|
||||
return before, after
|
||||
end
|
||||
return 0, 0
|
||||
end)
|
||||
end
|
||||
|
||||
---Create filter text
|
||||
---@return string
|
||||
entry.get_filter_text = function(self)
|
||||
return self.cache:ensure('get_filter_text', function()
|
||||
local word
|
||||
if misc.safe(self.completion_item.filterText) then
|
||||
word = self.completion_item.filterText
|
||||
else
|
||||
word = str.trim(self.completion_item.label)
|
||||
end
|
||||
|
||||
-- @see https://github.com/clangd/clangd/issues/815
|
||||
if misc.safe(self.completion_item.textEdit) then
|
||||
local diff = self.source_offset - self:get_offset()
|
||||
if diff > 0 then
|
||||
if char.is_symbol(string.byte(self.context.cursor_line, self:get_offset())) then
|
||||
local prefix = string.sub(self.context.cursor_line, self:get_offset(), self:get_offset() + diff)
|
||||
if string.find(word, prefix, 1, true) ~= 1 then
|
||||
word = prefix .. word
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return word
|
||||
end)
|
||||
end
|
||||
|
||||
---Get LSP's insert text
|
||||
---@return string
|
||||
entry.get_insert_text = function(self)
|
||||
return self.cache:ensure('get_insert_text', function()
|
||||
local word
|
||||
if misc.safe(self.completion_item.textEdit) then
|
||||
word = str.trim(self.completion_item.textEdit.newText)
|
||||
if self.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
|
||||
word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
|
||||
end
|
||||
elseif misc.safe(self.completion_item.insertText) then
|
||||
word = str.trim(self.completion_item.insertText)
|
||||
if self.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
|
||||
word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
|
||||
end
|
||||
else
|
||||
word = str.trim(self.completion_item.label)
|
||||
end
|
||||
return word
|
||||
end)
|
||||
end
|
||||
|
||||
---Make vim.CompletedItem
|
||||
---@param suggeset_offset number
|
||||
---@return vim.CompletedItem
|
||||
entry.get_vim_item = function(self, suggeset_offset)
|
||||
return self.cache:ensure({ 'get_vim_item', suggeset_offset }, function()
|
||||
local item = config.get().formatting.format(self, suggeset_offset)
|
||||
item.equal = 1
|
||||
item.empty = 1
|
||||
item.dup = self.completion_item.dup or 1
|
||||
item.user_data = { cmp = self.id }
|
||||
return item
|
||||
end)
|
||||
end
|
||||
|
||||
---Get commit characters
|
||||
---@return string[]
|
||||
entry.get_commit_characters = function(self)
|
||||
return misc.safe(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.completion_item.textEdit) then
|
||||
if misc.safe(self.completion_item.textEdit.insert) then
|
||||
insert_range = self.completion_item.textEdit.insert
|
||||
else
|
||||
insert_range = self.completion_item.textEdit.range
|
||||
end
|
||||
else
|
||||
insert_range = {
|
||||
start = {
|
||||
line = self.context.cursor.row - 1,
|
||||
character = math.min(vim.str_utfindex(self.context.cursor_line, self:get_offset() - 1), self.source_insert_range.start.character),
|
||||
},
|
||||
['end'] = self.source_insert_range['end'],
|
||||
}
|
||||
end
|
||||
return insert_range
|
||||
end
|
||||
|
||||
---Return replace range
|
||||
---@return vim.Range|nil
|
||||
entry.get_replace_range = function(self)
|
||||
return self.cache:ensure('get_replace_range', function()
|
||||
local replace_range
|
||||
if misc.safe(self.completion_item.textEdit) then
|
||||
if misc.safe(self.completion_item.textEdit.replace) then
|
||||
replace_range = self.completion_item.textEdit.replace
|
||||
else
|
||||
replace_range = self.completion_item.textEdit.range
|
||||
end
|
||||
else
|
||||
replace_range = {
|
||||
start = {
|
||||
line = self.source_replace_range.start.line,
|
||||
character = math.min(vim.str_utfindex(self.context.cursor_line, self:get_offset() - 1), self.source_replace_range.start.character),
|
||||
},
|
||||
['end'] = self.source_replace_range['end'],
|
||||
}
|
||||
end
|
||||
return replace_range
|
||||
end)
|
||||
end
|
||||
|
||||
---Get resolved completion item if possible.
|
||||
---@return lsp.CompletionItem
|
||||
entry.get_completion_item = function(self)
|
||||
if self.resolved_completion_item then
|
||||
return self.resolved_completion_item
|
||||
end
|
||||
return self.completion_item
|
||||
end
|
||||
|
||||
---Create documentation
|
||||
---@return string
|
||||
entry.get_documentation = function(self)
|
||||
local item = self:get_completion_item()
|
||||
|
||||
local documents = {}
|
||||
|
||||
-- detail
|
||||
if misc.safe(item.detail) and item.detail ~= '' then
|
||||
table.insert(documents, {
|
||||
kind = types.lsp.MarkupKind.Markdown,
|
||||
value = ('```%s\n%s\n```'):format(self.context.filetype, str.trim(item.detail)),
|
||||
})
|
||||
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)
|
||||
end
|
||||
|
||||
return vim.lsp.util.convert_input_to_markdown_lines(documents)
|
||||
end
|
||||
|
||||
---Get completion item kind
|
||||
---@return lsp.CompletionItemKind
|
||||
entry.get_kind = function(self)
|
||||
return misc.safe(self.completion_item.kind) or types.lsp.CompletionItemKind.Text
|
||||
end
|
||||
|
||||
---Execute completion item's command.
|
||||
---@param callback fun()
|
||||
entry.execute = function(self, callback)
|
||||
self.source:execute(self:get_completion_item(), callback)
|
||||
end
|
||||
|
||||
---Resolve completion item.
|
||||
---@param callback fun()
|
||||
entry.resolve = function(self, callback)
|
||||
if self.resolved_completion_item then
|
||||
return callback()
|
||||
end
|
||||
table.insert(self.resolved_callbacks, 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
|
||||
for _, c in ipairs(self.resolved_callbacks) do
|
||||
c()
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return entry
|
253
lua/cmp/entry_spec.lua
Normal file
253
lua/cmp/entry_spec.lua
Normal file
|
@ -0,0 +1,253 @@
|
|||
local spec = require('cmp.utils.spec')
|
||||
|
||||
local entry = require('cmp.entry')
|
||||
|
||||
describe('entry', function()
|
||||
before_each(spec.before)
|
||||
|
||||
it('one char', function()
|
||||
local state = spec.state('@.', 1, 3)
|
||||
local e = entry.new(state.press('@'), state.source(), {
|
||||
label = '@',
|
||||
})
|
||||
assert.are.equal(e:get_offset(), 3)
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, '@')
|
||||
end)
|
||||
|
||||
it('word length (no fix)', function()
|
||||
local state = spec.state('a.b', 1, 4)
|
||||
local e = entry.new(state.press('.'), state.source(), {
|
||||
label = 'b',
|
||||
})
|
||||
assert.are.equal(e:get_offset(), 5)
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'b')
|
||||
end)
|
||||
|
||||
it('word length (fix)', function()
|
||||
local state = spec.state('a.b', 1, 4)
|
||||
local e = entry.new(state.press('.'), state.source(), {
|
||||
label = 'b.',
|
||||
})
|
||||
assert.are.equal(e:get_offset(), 3)
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'b.')
|
||||
end)
|
||||
|
||||
it('semantic index (no fix)', function()
|
||||
local state = spec.state('a.bc', 1, 5)
|
||||
local e = entry.new(state.press('.'), state.source(), {
|
||||
label = 'c.',
|
||||
})
|
||||
assert.are.equal(e:get_offset(), 6)
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'c.')
|
||||
end)
|
||||
|
||||
it('semantic index (fix)', function()
|
||||
local state = spec.state('a.bc', 1, 5)
|
||||
local e = entry.new(state.press('.'), state.source(), {
|
||||
label = 'bc.',
|
||||
})
|
||||
assert.are.equal(e:get_offset(), 3)
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'bc.')
|
||||
end)
|
||||
|
||||
it('[vscode-html-language-server] 1', function()
|
||||
local state = spec.state(' </>', 1, 7)
|
||||
local e = entry.new(state.press('.'), state.source(), {
|
||||
label = '/div',
|
||||
textEdit = {
|
||||
range = {
|
||||
start = {
|
||||
line = 0,
|
||||
character = 0,
|
||||
},
|
||||
['end'] = {
|
||||
line = 0,
|
||||
character = 6,
|
||||
},
|
||||
},
|
||||
newText = ' </div',
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_offset(), 5)
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, '</div')
|
||||
end)
|
||||
|
||||
it('[clangd] 1', function()
|
||||
--NOTE: clangd does not return `.foo` as filterText but we should care about it.
|
||||
--nvim-cmp does care it by special handling in entry.lua.
|
||||
local state = spec.state('foo', 1, 4)
|
||||
local e = entry.new(state.press('.'), state.source(), {
|
||||
insertText = '->foo',
|
||||
label = ' foo',
|
||||
textEdit = {
|
||||
newText = '->foo',
|
||||
range = {
|
||||
start = {
|
||||
character = 3,
|
||||
line = 1,
|
||||
},
|
||||
['end'] = {
|
||||
character = 4,
|
||||
line = 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(4).word, '->foo')
|
||||
assert.are.equal(e:get_filter_text(), '.foo')
|
||||
end)
|
||||
|
||||
it('[typescript-language-server] 1', function()
|
||||
local state = spec.state('Promise.resolve()', 1, 18)
|
||||
local e = entry.new(state.press('.'), state.source(), {
|
||||
label = 'catch',
|
||||
})
|
||||
-- The offset will be 18 in this situation because the server returns `[Symbol]` as candidate.
|
||||
assert.are.equal(e:get_vim_item(18).word, '.catch')
|
||||
assert.are.equal(e:get_filter_text(), 'catch')
|
||||
end)
|
||||
|
||||
it('[typescript-language-server] 2', function()
|
||||
local state = spec.state('Promise.resolve()', 1, 18)
|
||||
local e = entry.new(state.press('.'), state.source(), {
|
||||
filterText = '.Symbol',
|
||||
label = 'Symbol',
|
||||
textEdit = {
|
||||
newText = '[Symbol]',
|
||||
range = {
|
||||
['end'] = {
|
||||
character = 18,
|
||||
line = 0,
|
||||
},
|
||||
start = {
|
||||
character = 17,
|
||||
line = 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(18).word, '[Symbol]')
|
||||
assert.are.equal(e:get_filter_text(), '.Symbol')
|
||||
end)
|
||||
|
||||
it('[lua-language-server] 1', function()
|
||||
local state = spec.state("local m = require'cmp.confi", 1, 28)
|
||||
local e
|
||||
|
||||
-- press g
|
||||
e = entry.new(state.press('g'), state.source(), {
|
||||
insertTextFormat = 2,
|
||||
label = 'cmp.config',
|
||||
textEdit = {
|
||||
newText = 'cmp.config',
|
||||
range = {
|
||||
['end'] = {
|
||||
character = 27,
|
||||
line = 1,
|
||||
},
|
||||
start = {
|
||||
character = 18,
|
||||
line = 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(19).word, 'cmp.config')
|
||||
assert.are.equal(e:get_filter_text(), 'cmp.config')
|
||||
|
||||
-- press '
|
||||
e = entry.new(state.press("'"), state.source(), {
|
||||
insertTextFormat = 2,
|
||||
label = 'cmp.config',
|
||||
textEdit = {
|
||||
newText = 'cmp.config',
|
||||
range = {
|
||||
['end'] = {
|
||||
character = 27,
|
||||
line = 1,
|
||||
},
|
||||
start = {
|
||||
character = 18,
|
||||
line = 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(19).word, 'cmp.config')
|
||||
assert.are.equal(e:get_filter_text(), 'cmp.config')
|
||||
end)
|
||||
|
||||
it('[lua-language-server] 2', function()
|
||||
local state = spec.state("local m = require'cmp.confi", 1, 28)
|
||||
local e
|
||||
|
||||
-- press g
|
||||
e = entry.new(state.press('g'), state.source(), {
|
||||
insertTextFormat = 2,
|
||||
label = 'lua.cmp.config',
|
||||
textEdit = {
|
||||
newText = 'lua.cmp.config',
|
||||
range = {
|
||||
['end'] = {
|
||||
character = 27,
|
||||
line = 1,
|
||||
},
|
||||
start = {
|
||||
character = 18,
|
||||
line = 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config')
|
||||
assert.are.equal(e:get_filter_text(), 'lua.cmp.config')
|
||||
|
||||
-- press '
|
||||
e = entry.new(state.press("'"), state.source(), {
|
||||
insertTextFormat = 2,
|
||||
label = 'lua.cmp.config',
|
||||
textEdit = {
|
||||
newText = 'lua.cmp.config',
|
||||
range = {
|
||||
['end'] = {
|
||||
character = 27,
|
||||
line = 1,
|
||||
},
|
||||
start = {
|
||||
character = 18,
|
||||
line = 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config')
|
||||
assert.are.equal(e:get_filter_text(), 'lua.cmp.config')
|
||||
end)
|
||||
|
||||
it('[intelephense] 1', function()
|
||||
local state = spec.state('\t\t', 1, 4)
|
||||
|
||||
-- press g
|
||||
local e = entry.new(state.press('$'), state.source(), {
|
||||
detail = '\\Nico_URLConf',
|
||||
kind = 6,
|
||||
label = '$this',
|
||||
sortText = '$this',
|
||||
textEdit = {
|
||||
newText = '$this',
|
||||
range = {
|
||||
['end'] = {
|
||||
character = 3,
|
||||
line = 1,
|
||||
},
|
||||
start = {
|
||||
character = 2,
|
||||
line = 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, '$this')
|
||||
assert.are.equal(e:get_filter_text(), '$this')
|
||||
end)
|
||||
end)
|
112
lua/cmp/float.lua
Normal file
112
lua/cmp/float.lua
Normal file
|
@ -0,0 +1,112 @@
|
|||
local async = require('cmp.utils.async')
|
||||
local config = require('cmp.config')
|
||||
|
||||
---@class cmp.Float
|
||||
---@field public entry cmp.Entry|nil
|
||||
---@field public buf number|nil
|
||||
---@field public win number|nil
|
||||
local float = {}
|
||||
|
||||
---Create new floating window module
|
||||
float.new = function()
|
||||
local self = setmetatable({}, { __index = float })
|
||||
self.entry = nil
|
||||
self.win = nil
|
||||
self.buf = nil
|
||||
return self
|
||||
end
|
||||
|
||||
---Show floating window
|
||||
---@param e cmp.Entry
|
||||
float.show = function(self, e)
|
||||
float.close.stop()
|
||||
|
||||
local documentation = config.get().documentation
|
||||
|
||||
-- update buffer content if needed.
|
||||
if not self.entry or e.id ~= self.entry.id then
|
||||
self.entry = e
|
||||
self.buf = vim.api.nvim_create_buf(true, true)
|
||||
vim.api.nvim_buf_set_option(self.buf, 'bufhidden', 'wipe')
|
||||
|
||||
local documents = e:get_documentation()
|
||||
if #documents == 0 then
|
||||
return self:close()
|
||||
end
|
||||
vim.lsp.util.stylize_markdown(self.buf, documents, {
|
||||
max_width = documentation.maxwidth,
|
||||
max_height = documentation.maxheight,
|
||||
})
|
||||
end
|
||||
|
||||
local width, height = vim.lsp.util._make_floating_popup_size(vim.api.nvim_buf_get_lines(self.buf, 0, -1, false), {
|
||||
max_width = documentation.maxwidth,
|
||||
max_height = documentation.maxheight,
|
||||
})
|
||||
if width <= 0 or height <= 0 then
|
||||
return self:close()
|
||||
end
|
||||
|
||||
local pum = vim.fn.pum_getpos() or {}
|
||||
if not pum.col then
|
||||
return self:close()
|
||||
end
|
||||
|
||||
local right_col = pum.col + pum.width + (pum.scrollbar and 1 or 0)
|
||||
local right_space = vim.o.columns - right_col - 1
|
||||
local left_col = pum.col - width - 3 -- TODO: Why is this needed -3?
|
||||
local left_space = pum.col - 1
|
||||
|
||||
local col
|
||||
if right_space >= width and left_space >= width then
|
||||
if right_space < left_space then
|
||||
col = left_col
|
||||
else
|
||||
col = right_col
|
||||
end
|
||||
elseif right_space >= width then
|
||||
col = right_col
|
||||
elseif left_space >= width then
|
||||
col = left_col
|
||||
else
|
||||
return self:close()
|
||||
end
|
||||
|
||||
local style = {
|
||||
relative = 'editor',
|
||||
style = 'minimal',
|
||||
width = width,
|
||||
height = height,
|
||||
row = pum.row,
|
||||
col = col,
|
||||
border = documentation.border,
|
||||
}
|
||||
|
||||
if self.win and vim.api.nvim_win_is_valid(self.win) then
|
||||
vim.api.nvim_win_set_buf(self.win, self.buf)
|
||||
vim.api.nvim_win_set_config(self.win, style)
|
||||
else
|
||||
self.win = vim.api.nvim_open_win(self.buf, false, style)
|
||||
vim.api.nvim_win_set_option(self.win, 'conceallevel', 2)
|
||||
vim.api.nvim_win_set_option(self.win, 'concealcursor', 'n')
|
||||
vim.api.nvim_win_set_option(self.win, 'winhighlight', config.get().documentation.winhighlight)
|
||||
vim.api.nvim_win_set_option(self.win, 'foldenable', false)
|
||||
vim.api.nvim_win_set_option(self.win, 'wrap', true)
|
||||
vim.api.nvim_win_set_option(self.win, 'scrolloff', 0)
|
||||
end
|
||||
end
|
||||
|
||||
---Close floating window
|
||||
float.close = async.throttle(
|
||||
vim.schedule_wrap(function(self)
|
||||
if self.win and vim.api.nvim_win_is_valid(self.win) then
|
||||
vim.api.nvim_win_close(self.win, true)
|
||||
end
|
||||
self.entry = nil
|
||||
self.buf = nil
|
||||
self.win = nil
|
||||
end),
|
||||
0
|
||||
)
|
||||
|
||||
return float
|
79
lua/cmp/init.lua
Normal file
79
lua/cmp/init.lua
Normal file
|
@ -0,0 +1,79 @@
|
|||
local core = require('cmp.core')
|
||||
local types = require('cmp.types')
|
||||
local source = require('cmp.source')
|
||||
local config = require('cmp.config')
|
||||
local autocmd = require('cmp.autocmd')
|
||||
|
||||
local cmp = {}
|
||||
|
||||
---Expose types
|
||||
for k, v in pairs(require('cmp.types.cmp')) do
|
||||
cmp[k] = v
|
||||
end
|
||||
cmp.lsp = require('cmp.types.lsp')
|
||||
cmp.vim = require('cmp.types.vim')
|
||||
|
||||
---Register completion sources
|
||||
---@param name string
|
||||
---@param s cmp.Source
|
||||
---@return number
|
||||
cmp.register_source = function(name, s)
|
||||
local src = source.new(name, s)
|
||||
core.register_source(src)
|
||||
return src.id
|
||||
end
|
||||
|
||||
---Unregister completion source
|
||||
---@param id number
|
||||
cmp.unregister_source = function(id)
|
||||
core.unregister_source(id)
|
||||
end
|
||||
|
||||
---@type cmp.Setup
|
||||
cmp.setup = setmetatable({
|
||||
global = function(c)
|
||||
config.set_global(c)
|
||||
end,
|
||||
buffer = function(c)
|
||||
config.set_buffer(c, vim.api.nvim_get_current_buf())
|
||||
end,
|
||||
}, {
|
||||
__call = function(self, c)
|
||||
self.global(c)
|
||||
end,
|
||||
})
|
||||
|
||||
---Invoke completion manually
|
||||
cmp.complete = function()
|
||||
core.complete(core.get_context({
|
||||
reason = types.cmp.ContextReason.Manual,
|
||||
}))
|
||||
end
|
||||
|
||||
---Close completion
|
||||
cmp.close = function()
|
||||
core.reset()
|
||||
end
|
||||
|
||||
---Internal expand snippet function.
|
||||
---TODO: It should be removed when we remove `autoload/cmp.vim`.
|
||||
---@param args cmp.SnippetExpansionParams
|
||||
cmp._expand_snippet = function(args)
|
||||
return config.get().snippet.expand(args)
|
||||
end
|
||||
|
||||
---Handle events
|
||||
autocmd.subscribe('InsertEnter', function()
|
||||
core.prepare()
|
||||
core.autocomplete('InsertEnter')
|
||||
end)
|
||||
|
||||
autocmd.subscribe('TextChanged', function()
|
||||
core.autocomplete('TextChanged')
|
||||
end)
|
||||
|
||||
autocmd.subscribe('InsertLeave', function()
|
||||
core.reset()
|
||||
end)
|
||||
|
||||
return cmp
|
248
lua/cmp/matcher.lua
Normal file
248
lua/cmp/matcher.lua
Normal file
|
@ -0,0 +1,248 @@
|
|||
local char = require('cmp.utils.char')
|
||||
|
||||
local matcher = {}
|
||||
|
||||
matcher.WORD_BOUNDALY_ORDER_FACTOR = 5
|
||||
|
||||
matcher.PREFIX_FACTOR = 8
|
||||
matcher.NOT_FUZZY_FACTOR = 6
|
||||
|
||||
---@type function
|
||||
matcher.debug = function(...)
|
||||
return ...
|
||||
end
|
||||
|
||||
--- score
|
||||
--
|
||||
-- ### The score
|
||||
--
|
||||
-- The `score` is `matched char count` generally.
|
||||
--
|
||||
-- But cmp will fix the score with some of the below points so the actual score is not `matched char count`.
|
||||
--
|
||||
-- 1. Word boundary order
|
||||
--
|
||||
-- cmp prefers the match that near by word-beggining.
|
||||
--
|
||||
-- 2. Strict case
|
||||
--
|
||||
-- cmp prefers strict match than ignorecase match.
|
||||
--
|
||||
--
|
||||
-- ### Matching specs.
|
||||
--
|
||||
-- 1. Prefix matching per word boundary
|
||||
--
|
||||
-- `bora` -> `border-radius` # imaginary score: 4
|
||||
-- ^^~~ ^^ ~~
|
||||
--
|
||||
-- 2. Try sequential match first
|
||||
--
|
||||
-- `woroff` -> `word_offset` # imaginary score: 6
|
||||
-- ^^^~~~ ^^^ ~~~
|
||||
--
|
||||
-- * The `woroff`'s second `o` should not match `word_offset`'s first `o`
|
||||
--
|
||||
-- 3. Prefer early word boundary
|
||||
--
|
||||
-- `call` -> `call` # imaginary score: 4.1
|
||||
-- ^^^^ ^^^^
|
||||
-- `call` -> `condition_all` # imaginary score: 4
|
||||
-- ^~~~ ^ ~~~
|
||||
--
|
||||
-- 4. Prefer strict match
|
||||
--
|
||||
-- `Buffer` -> `Buffer` # imaginary score: 6.1
|
||||
-- ^^^^^^ ^^^^^^
|
||||
-- `buffer` -> `Buffer` # imaginary score: 6
|
||||
-- ^^^^^^ ^^^^^^
|
||||
--
|
||||
-- 5. Use remaining characters for substring match
|
||||
--
|
||||
-- `fmodify` -> `fnamemodify` # imaginary score: 1
|
||||
-- ^~~~~~~ ^ ~~~~~~
|
||||
--
|
||||
-- 6. Avoid unexpected match detection
|
||||
--
|
||||
-- `candlesingle` -> candle#accept#single
|
||||
-- ^^^^^^~~~~~~ ^^^^^^ ~~~~~~
|
||||
--
|
||||
-- * The `accept`'s `a` should not match to `candle`'s `a`
|
||||
--
|
||||
---Match entry
|
||||
---@param input string
|
||||
---@param word string
|
||||
---@return number
|
||||
matcher.match = function(input, word)
|
||||
-- Empty input
|
||||
if #input == 0 then
|
||||
return matcher.PREFIX_FACTOR + matcher.NOT_FUZZY_FACTOR
|
||||
end
|
||||
|
||||
-- Ignore if input is long than word
|
||||
if #input > #word then
|
||||
return 0
|
||||
end
|
||||
|
||||
--- Gather matched regions
|
||||
local matches = {}
|
||||
local input_start_index = 1
|
||||
local input_end_index = 1
|
||||
local word_index = 1
|
||||
local word_bound_index = 1
|
||||
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
|
||||
word_index = char.get_next_semantic_index(word, m.word_match_end)
|
||||
table.insert(matches, m)
|
||||
else
|
||||
word_index = char.get_next_semantic_index(word, word_index)
|
||||
end
|
||||
word_bound_index = word_bound_index + 1
|
||||
end
|
||||
|
||||
if #matches == 0 then
|
||||
return 0
|
||||
end
|
||||
|
||||
-- Compute prefix match score
|
||||
local score = 0
|
||||
local idx = 1
|
||||
for _, m in ipairs(matches) do
|
||||
local s = 0
|
||||
for i = math.max(idx, m.input_match_start), m.input_match_end do
|
||||
s = s + 1
|
||||
idx = i
|
||||
end
|
||||
idx = idx + 1
|
||||
if s > 0 then
|
||||
score = score + (s * (1 + math.max(0, matcher.WORD_BOUNDALY_ORDER_FACTOR - m.index) / matcher.WORD_BOUNDALY_ORDER_FACTOR))
|
||||
score = score + (m.strict_match and 0.1 or 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- Add prefix bonus
|
||||
score = score + ((matches[1].input_match_start == 1 and matches[1].word_match_start == 1) and matcher.PREFIX_FACTOR or 0)
|
||||
|
||||
-- Check remaining input as fuzzy
|
||||
if matches[#matches].input_match_end < #input then
|
||||
if matcher.fuzzy(input, word, matches) then
|
||||
return score
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
return score + matcher.NOT_FUZZY_FACTOR
|
||||
end
|
||||
|
||||
--- fuzzy
|
||||
matcher.fuzzy = function(input, word, matches)
|
||||
local last_match = matches[#matches]
|
||||
|
||||
-- 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]
|
||||
local word_offset = 0
|
||||
local word_index = char.get_next_semantic_index(word, curr_match.word_match_end)
|
||||
while word_offset + word_index < next_match.word_match_start and input_index <= #input do
|
||||
if char.match(string.byte(word, word_index + word_offset), string.byte(input, input_index)) then
|
||||
input_index = input_index + 1
|
||||
word_offset = word_offset + 1
|
||||
else
|
||||
word_index = char.get_next_semantic_index(word, word_index + word_offset)
|
||||
word_offset = 0
|
||||
end
|
||||
end
|
||||
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
|
||||
while word_offset + word_index <= #word and input_index <= #input do
|
||||
if char.match(string.byte(word, word_index + word_offset), string.byte(input, input_index)) then
|
||||
matched = true
|
||||
input_index = input_index + 1
|
||||
elseif matched then
|
||||
input_index = last_input_index
|
||||
end
|
||||
word_offset = word_offset + 1
|
||||
end
|
||||
if input_index >= #input then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- find_match_region
|
||||
matcher.find_match_region = function(input, input_start_index, input_end_index, word, word_index)
|
||||
-- determine input position ( woroff -> word_offset )
|
||||
while input_start_index < input_end_index do
|
||||
if char.match(string.byte(input, input_end_index), string.byte(word, word_index)) then
|
||||
break
|
||||
end
|
||||
input_end_index = input_end_index - 1
|
||||
end
|
||||
|
||||
-- Can't determine input position
|
||||
if input_end_index < input_start_index then
|
||||
return nil
|
||||
end
|
||||
|
||||
local strict_match_count = 0
|
||||
local input_match_start = -1
|
||||
local input_index = input_end_index
|
||||
local word_offset = 0
|
||||
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)
|
||||
if char.match(c1, c2) then
|
||||
-- Match start.
|
||||
if input_match_start == -1 then
|
||||
input_match_start = input_index
|
||||
end
|
||||
|
||||
-- Increase strict_match_count
|
||||
if c1 == c2 then
|
||||
strict_match_count = strict_match_count + 1
|
||||
end
|
||||
|
||||
word_offset = word_offset + 1
|
||||
else
|
||||
-- Match end (partial region)
|
||||
if input_match_start ~= -1 then
|
||||
return {
|
||||
input_match_start = input_match_start,
|
||||
input_match_end = input_index - 1,
|
||||
word_match_start = word_index,
|
||||
word_match_end = word_index + word_offset - 1,
|
||||
strict_match = strict_match_count == input_index - input_match_start,
|
||||
}
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
input_index = input_index + 1
|
||||
end
|
||||
|
||||
-- Match end (whole region)
|
||||
if input_match_start ~= -1 then
|
||||
return {
|
||||
input_match_start = input_match_start,
|
||||
input_match_end = input_index - 1,
|
||||
word_match_start = word_index,
|
||||
word_match_end = word_index + word_offset - 1,
|
||||
strict_match = strict_match_count == input_index - input_match_start,
|
||||
}
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
return matcher
|
33
lua/cmp/matcher_spec.lua
Normal file
33
lua/cmp/matcher_spec.lua
Normal file
|
@ -0,0 +1,33 @@
|
|||
local spec = require('cmp.utils.spec')
|
||||
|
||||
local matcher = require('cmp.matcher')
|
||||
|
||||
describe('matcher', function()
|
||||
before_each(spec.before)
|
||||
|
||||
it('match', function()
|
||||
assert.is.truthy(matcher.match('', 'a') >= 1)
|
||||
assert.is.truthy(matcher.match('a', 'a') >= 1)
|
||||
assert.is.truthy(matcher.match('ab', 'a') == 0)
|
||||
assert.is.truthy(matcher.match('ab', 'ab') > matcher.match('ab', 'a_b'))
|
||||
assert.is.truthy(matcher.match('ab', 'a_b_c') > matcher.match('ac', 'a_b_c'))
|
||||
|
||||
assert.is.truthy(matcher.match('bora', 'border-radius') >= 1)
|
||||
assert.is.truthy(matcher.match('woroff', 'word_offset') >= 1)
|
||||
assert.is.truthy(matcher.match('call', 'call') > matcher.match('call', 'condition_all'))
|
||||
assert.is.truthy(matcher.match('Buffer', 'Buffer') > matcher.match('Buffer', 'buffer'))
|
||||
assert.is.truthy(matcher.match('fmodify', 'fnamemodify') >= 1)
|
||||
assert.is.truthy(matcher.match('candlesingle', 'candle#accept#single') >= 1)
|
||||
assert.is.truthy(matcher.match('conso', 'console') > matcher.match('conso', 'ConstantSourceNode'))
|
||||
assert.is.truthy(matcher.match('var_', 'var_dump') >= 1)
|
||||
end)
|
||||
|
||||
it('debug', function()
|
||||
assert.is.truthy(true)
|
||||
|
||||
matcher.debug = function(...)
|
||||
print(vim.inspect({ ... }))
|
||||
end
|
||||
print('score', matcher.match('vsnipnextjump', 'vsnip-jump-next'))
|
||||
end)
|
||||
end)
|
242
lua/cmp/menu.lua
Normal file
242
lua/cmp/menu.lua
Normal file
|
@ -0,0 +1,242 @@
|
|||
local debug = require('cmp.utils.debug')
|
||||
local async = require('cmp.utils.async')
|
||||
local float = require('cmp.float')
|
||||
local types = require('cmp.types')
|
||||
local config = require('cmp.config')
|
||||
local autocmd = require('cmp.autocmd')
|
||||
|
||||
---@class cmp.Menu
|
||||
---@field public float cmp.Float
|
||||
---@field public cache cmp.Cache
|
||||
---@field public offset number
|
||||
---@field public on_select fun(e: cmp.Entry)
|
||||
---@field public items vim.CompletedItem[]
|
||||
---@field public entries cmp.Entry[]
|
||||
---@field public entry_map table<number, cmp.Entry>
|
||||
---@field public selected_entry cmp.Entry|nil
|
||||
---@field public context cmp.Context
|
||||
---@field public resolve_dedup fun(callback: function)
|
||||
local menu = {}
|
||||
|
||||
---Create menu
|
||||
---@return cmp.Menu
|
||||
menu.new = function()
|
||||
local self = setmetatable({}, { __index = menu })
|
||||
self.float = float.new()
|
||||
self.resolve_dedup = async.dedup()
|
||||
self.on_select = function() end
|
||||
self:reset()
|
||||
autocmd.subscribe('CompleteChanged', function()
|
||||
local e = self:get_selected_entry()
|
||||
if e then
|
||||
self:select(e)
|
||||
else
|
||||
self:unselect()
|
||||
end
|
||||
end)
|
||||
return self
|
||||
end
|
||||
|
||||
---Close menu
|
||||
menu.close = function(self)
|
||||
if vim.fn.pumvisible() == 1 then
|
||||
vim.fn.complete(1, {})
|
||||
end
|
||||
self:unselect()
|
||||
end
|
||||
|
||||
---Reset menu
|
||||
menu.reset = function(self)
|
||||
self.offset = nil
|
||||
self.items = {}
|
||||
self.entries = {}
|
||||
self.entry_map = {}
|
||||
self.context = nil
|
||||
self.preselect = 0
|
||||
self:close()
|
||||
end
|
||||
|
||||
---Update menu
|
||||
---@param ctx cmp.Context
|
||||
---@param sources cmp.Source[]
|
||||
---@return cmp.Menu
|
||||
menu.update = function(self, ctx, sources)
|
||||
if not (ctx.mode == 'i' or ctx.mode == 'ic') then
|
||||
return
|
||||
end
|
||||
|
||||
local entries = {}
|
||||
local entry_map = {}
|
||||
|
||||
-- check the source triggered by character
|
||||
local has_triggered_by_character_source = false
|
||||
for _, s in ipairs(sources) do
|
||||
if s:has_items() then
|
||||
if s.trigger_kind == types.lsp.CompletionTriggerKind.TriggerCharacter then
|
||||
has_triggered_by_character_source = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- create filtered entries.
|
||||
local offset = ctx.cursor.col
|
||||
for i, s in ipairs(sources) do
|
||||
if s:has_items() and s.offset <= offset then
|
||||
if not has_triggered_by_character_source or s.trigger_kind == types.lsp.CompletionTriggerKind.TriggerCharacter then
|
||||
-- source order priority bonus.
|
||||
local priority = (#sources - i - 1) * 2
|
||||
|
||||
local filtered = s:get_entries(ctx)
|
||||
for _, e in ipairs(filtered) do
|
||||
e.score = e.score + priority
|
||||
table.insert(entries, e)
|
||||
entry_map[e.id] = e
|
||||
end
|
||||
if #filtered > 0 then
|
||||
offset = math.min(offset, s.offset)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- sort.
|
||||
config.get().sorting.sort(entries)
|
||||
|
||||
-- create vim items.
|
||||
local items = {}
|
||||
local abbrs = {}
|
||||
local preselect = 0
|
||||
for i, e in ipairs(entries) do
|
||||
if preselect == 0 and e.completion_item.preselect then
|
||||
preselect = i
|
||||
end
|
||||
|
||||
local item = e:get_vim_item(offset)
|
||||
if not abbrs[item.abbr] or item.dup == 1 then
|
||||
table.insert(items, item)
|
||||
abbrs[item.abbr] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- save recent pum state.
|
||||
self.offset = offset
|
||||
self.items = items
|
||||
self.entries = entries
|
||||
self.entry_map = entry_map
|
||||
self.preselect = preselect
|
||||
self.context = ctx
|
||||
self:show()
|
||||
|
||||
if #self.entries == 0 then
|
||||
self:unselect()
|
||||
end
|
||||
end
|
||||
|
||||
---Restore previous menu
|
||||
---@param ctx cmp.Context
|
||||
menu.restore = function(self, ctx)
|
||||
if not (ctx.mode == 'i' or ctx.mode == 'ic') then
|
||||
return
|
||||
end
|
||||
|
||||
if not ctx.pumvisible then
|
||||
if #self.items > 0 then
|
||||
if self.offset <= ctx.cursor.col then
|
||||
debug.log('menu/restore')
|
||||
self:show()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Show completion item
|
||||
menu.show = function(self)
|
||||
if vim.fn.pumvisible() == 0 and #self.entries == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local completeopt = vim.o.completeopt
|
||||
if self.preselect == 1 then
|
||||
vim.cmd('set completeopt=menuone,noinsert')
|
||||
else
|
||||
vim.cmd('set completeopt=' .. config.get().completion.completeopt)
|
||||
end
|
||||
vim.fn.complete(self.offset, self.items)
|
||||
vim.cmd('set completeopt=' .. completeopt)
|
||||
|
||||
if self.preselect > 0 then
|
||||
vim.api.nvim_select_popupmenu_item(self.preselect - 1, false, false, {})
|
||||
end
|
||||
end
|
||||
|
||||
---Select current item
|
||||
---@param e cmp.Entry
|
||||
menu.select = function(self, e)
|
||||
-- Documentation (always invoke to follow to the pum position)
|
||||
e:resolve(self.resolve_dedup(vim.schedule_wrap(function()
|
||||
if self:get_selected_entry() == e then
|
||||
self.float:show(e)
|
||||
end
|
||||
end)))
|
||||
|
||||
self.on_select(e)
|
||||
end
|
||||
|
||||
---Select current item
|
||||
menu.unselect = function(self)
|
||||
self.float:close()
|
||||
end
|
||||
|
||||
---Geta current active entry
|
||||
---@return cmp.Entry|nil
|
||||
menu.get_active_entry = function(self)
|
||||
local completed_item = vim.v.completed_item or {}
|
||||
if vim.fn.pumvisible() == 0 or not completed_item.user_data then
|
||||
return nil
|
||||
end
|
||||
|
||||
local id = completed_item.user_data.cmp
|
||||
if id then
|
||||
return self.entry_map[id]
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---Get current selected entry
|
||||
---@return cmp.Entry|nil
|
||||
menu.get_selected_entry = function(self)
|
||||
local info = vim.fn.complete_info({ 'items', 'selected' })
|
||||
if info.selected == -1 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local completed_item = info.items[math.max(info.selected, 0) + 1] or {}
|
||||
if not completed_item.user_data then
|
||||
return nil
|
||||
end
|
||||
|
||||
local id = completed_item.user_data.cmp
|
||||
if id then
|
||||
return self.entry_map[id]
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---Get first entry
|
||||
---@param self cmp.Entry|nil
|
||||
menu.get_first_entry = function(self)
|
||||
local info = vim.fn.complete_info({ 'items' })
|
||||
local completed_item = info.items[1] or {}
|
||||
if not completed_item.user_data then
|
||||
return nil
|
||||
end
|
||||
|
||||
local id = completed_item.user_data.cmp
|
||||
if id then
|
||||
return self.entry_map[id]
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
return menu
|
281
lua/cmp/source.lua
Normal file
281
lua/cmp/source.lua
Normal file
|
@ -0,0 +1,281 @@
|
|||
local context = require('cmp.context')
|
||||
local config = require('cmp.config')
|
||||
local matcher = require('cmp.matcher')
|
||||
local entry = require('cmp.entry')
|
||||
local debug = require('cmp.utils.debug')
|
||||
local misc = require('cmp.utils.misc')
|
||||
local cache = require('cmp.utils.cache')
|
||||
local types = require('cmp.types')
|
||||
local async = require('cmp.utils.async')
|
||||
local pattern = require('cmp.utils.pattern')
|
||||
|
||||
---@class cmp.Source
|
||||
---@field public id number
|
||||
---@field public name string
|
||||
---@field public source any
|
||||
---@field public cache cmp.Cache
|
||||
---@field public revision number
|
||||
---@field public context cmp.Context
|
||||
---@field public trigger_kind lsp.CompletionTriggerKind|nil
|
||||
---@field public incomplete boolean
|
||||
---@field public entries cmp.Entry[]
|
||||
---@field public offset number|nil
|
||||
---@field public status cmp.SourceStatus
|
||||
---@field public complete_dedup function
|
||||
local source = {}
|
||||
|
||||
---@alias cmp.SourceStatus "1" | "2" | "3"
|
||||
source.SourceStatus = {}
|
||||
source.SourceStatus.WAITING = 1
|
||||
source.SourceStatus.FETCHING = 2
|
||||
source.SourceStatus.COMPLETED = 3
|
||||
|
||||
---@alias cmp.SourceChangeKind "1" | "2" | "3"
|
||||
source.SourceChangeKind = {}
|
||||
source.SourceChangeKind.RETRIEVE = 1
|
||||
source.SourceChangeKind.CONTINUE = 2
|
||||
|
||||
---@return cmp.Source
|
||||
source.new = function(name, s)
|
||||
local self = setmetatable({}, { __index = source })
|
||||
self.id = misc.id('source')
|
||||
self.name = name
|
||||
self.source = s
|
||||
self.cache = cache.new()
|
||||
self.complete_dedup = async.dedup()
|
||||
self.revision = 0
|
||||
self:reset()
|
||||
return self
|
||||
end
|
||||
|
||||
---Reset current completion state
|
||||
---@return boolean
|
||||
source.reset = function(self)
|
||||
self.cache:clear()
|
||||
self.revision = self.revision + 1
|
||||
self.context = context.empty()
|
||||
self.trigger_kind = nil
|
||||
self.incomplete = false
|
||||
self.entries = {}
|
||||
self.offset = -1
|
||||
self.status = source.SourceStatus.WAITING
|
||||
self.complete_dedup(function() end)
|
||||
end
|
||||
|
||||
---Return source option
|
||||
---@return table
|
||||
source.get_option = function(self)
|
||||
return config.get_source_option(self.name)
|
||||
end
|
||||
|
||||
---Return the source has items or not.
|
||||
---@return boolean
|
||||
source.has_items = function(self)
|
||||
return self.offset ~= -1
|
||||
end
|
||||
|
||||
---Get fetching time
|
||||
source.get_fetching_time = function(self)
|
||||
if self.status == source.SourceStatus.FETCHING then
|
||||
return vim.loop.now() - self.context.time
|
||||
end
|
||||
return 100 * 1000 -- return pseudo time if source isn't fetching.
|
||||
end
|
||||
|
||||
---Return filtered entries
|
||||
---@param ctx cmp.Context
|
||||
---@return cmp.Entry[]
|
||||
source.get_entries = function(self, ctx)
|
||||
if not self:has_items() then
|
||||
return {}
|
||||
end
|
||||
|
||||
local prev_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
|
||||
end
|
||||
return nil
|
||||
end)()
|
||||
|
||||
return self.cache:ensure({ 'get_entries', self.revision, ctx.cursor_before_line }, function()
|
||||
debug.log('filter', self.name, self.id, #(prev_entries or self.entries))
|
||||
|
||||
local inputs = {}
|
||||
local entries = {}
|
||||
for _, e in ipairs(prev_entries or self.entries) do
|
||||
local o = e:get_offset()
|
||||
if not inputs[o] then
|
||||
inputs[o] = string.sub(ctx.cursor_before_line, o)
|
||||
end
|
||||
e.score = matcher.match(inputs[o], e:get_filter_text())
|
||||
e.exact = inputs[o] == e:get_filter_text()
|
||||
if e.score >= 1 then
|
||||
table.insert(entries, e)
|
||||
end
|
||||
end
|
||||
return entries
|
||||
end)
|
||||
end
|
||||
|
||||
---Get default insert range
|
||||
---@return lsp.Range|nil
|
||||
source.get_default_insert_range = function(self)
|
||||
if not self.context then
|
||||
return nil
|
||||
end
|
||||
|
||||
return self.cache:ensure({ 'get_default_insert_range', self.revision }, function()
|
||||
return {
|
||||
start = {
|
||||
line = self.context.cursor.row - 1,
|
||||
character = vim.str_utfindex(self.context.cursor_line, self.offset - 1),
|
||||
},
|
||||
['end'] = {
|
||||
line = self.context.cursor.row - 1,
|
||||
character = vim.str_utfindex(self.context.cursor_line, self.context.cursor.col - 1),
|
||||
},
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
---Get default replace range
|
||||
---@return lsp.Range|nil
|
||||
source.get_default_replace_range = function(self)
|
||||
if not self.context then
|
||||
return nil
|
||||
end
|
||||
|
||||
return self.cache:ensure({ 'get_default_replace_range', 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 = vim.str_utfindex(self.context.cursor_line, self.offset - 1),
|
||||
},
|
||||
['end'] = {
|
||||
line = self.context.cursor.row - 1,
|
||||
character = vim.str_utfindex(self.context.cursor_line, e and self.offset + e - 2 or self.context.cursor.col - 1),
|
||||
},
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
---Get keyword_pattern
|
||||
---@return string
|
||||
source.get_keyword_pattern = function(self)
|
||||
if self.source.get_keyword_pattern then
|
||||
return self.source:get_keyword_pattern()
|
||||
end
|
||||
return config.get().completion.keyword_pattern
|
||||
end
|
||||
|
||||
---Get trigger_characters
|
||||
---@return string[]
|
||||
source.get_trigger_characters = function(self)
|
||||
if self.source.get_trigger_characters then
|
||||
return self.source:get_trigger_characters() or {}
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
---Invoke completion
|
||||
---@param ctx cmp.Context
|
||||
---@param callback function
|
||||
---@return boolean Return true if not trigger completion.
|
||||
source.complete = function(self, ctx, callback)
|
||||
local c = config.get()
|
||||
|
||||
local offset = ctx:get_offset(self:get_keyword_pattern())
|
||||
if offset == ctx.cursor.col then
|
||||
self:reset()
|
||||
end
|
||||
|
||||
local completion_context
|
||||
if ctx:get_reason() == types.cmp.ContextReason.Manual then
|
||||
completion_context = {
|
||||
triggerKind = types.lsp.CompletionTriggerKind.Invoked,
|
||||
}
|
||||
elseif vim.tbl_contains(self:get_trigger_characters(), ctx.before_char) then
|
||||
completion_context = {
|
||||
triggerKind = types.lsp.CompletionTriggerKind.TriggerCharacter,
|
||||
triggerCharacter = ctx.before_char,
|
||||
}
|
||||
elseif c.completion.keyword_length <= (ctx.cursor.col - offset) and self.offset ~= offset then
|
||||
completion_context = {
|
||||
triggerKind = types.lsp.CompletionTriggerKind.Invoked,
|
||||
}
|
||||
elseif self.incomplete and offset ~= ctx.cursor.col then
|
||||
completion_context = {
|
||||
triggerKind = types.lsp.CompletionTriggerKind.TriggerForIncompleteCompletions,
|
||||
}
|
||||
end
|
||||
if not completion_context then
|
||||
debug.log('skip empty context', self.name, self.id)
|
||||
return
|
||||
end
|
||||
|
||||
debug.log('request', self.name, self.id, offset, vim.inspect(completion_context))
|
||||
local prev_status = self.status
|
||||
self.status = source.SourceStatus.FETCHING
|
||||
self.offset = offset
|
||||
self.context = ctx
|
||||
self.source:complete(
|
||||
{
|
||||
context = ctx,
|
||||
offset = self.offset,
|
||||
option = self:get_option(),
|
||||
completion_context = completion_context,
|
||||
},
|
||||
vim.schedule_wrap(self.complete_dedup(function(response)
|
||||
self.revision = self.revision + 1
|
||||
if #(misc.safe(response) and response.items or response or {}) > 0 then
|
||||
debug.log('retrieve', self.name, self.id, #(response.items or response))
|
||||
self.status = source.SourceStatus.COMPLETED
|
||||
self.trigger_kind = completion_context.triggerKind
|
||||
self.incomplete = response.isIncomplete or false
|
||||
self.entries = {}
|
||||
for i, item in ipairs(response.items or response) do
|
||||
local e = entry.new(ctx, self, item)
|
||||
self.entries[i] = e
|
||||
self.offset = math.min(self.offset, e:get_offset())
|
||||
end
|
||||
else
|
||||
debug.log('continue', self.name, self.id, 'nil')
|
||||
self.status = prev_status
|
||||
end
|
||||
callback()
|
||||
end))
|
||||
)
|
||||
return true
|
||||
end
|
||||
|
||||
---Resolve CompletionItem
|
||||
---@param item lsp.CompletionItem
|
||||
---@param callback fun(item: lsp.CompletionItem)
|
||||
source.resolve = function(self, item, callback)
|
||||
if not self.source.resolve then
|
||||
return callback(item)
|
||||
end
|
||||
self.source:resolve(item, function(resolved_item)
|
||||
callback(resolved_item or item)
|
||||
end)
|
||||
end
|
||||
|
||||
---Execute command
|
||||
---@param item lsp.CompletionItem
|
||||
---@param callback fun()
|
||||
source.execute = function(self, item, callback)
|
||||
if not self.source.execute then
|
||||
return callback()
|
||||
end
|
||||
self.source:execute(item, function()
|
||||
callback()
|
||||
end)
|
||||
end
|
||||
|
||||
return source
|
11
lua/cmp/source_spec.lua
Normal file
11
lua/cmp/source_spec.lua
Normal file
|
@ -0,0 +1,11 @@
|
|||
local spec = require('cmp.utils.spec')
|
||||
|
||||
-- local source = require "cmp.source"
|
||||
|
||||
describe('source', function()
|
||||
before_each(spec.before)
|
||||
|
||||
it('new', function()
|
||||
-- local s = source.new()
|
||||
end)
|
||||
end)
|
84
lua/cmp/types/cmp.lua
Normal file
84
lua/cmp/types/cmp.lua
Normal file
|
@ -0,0 +1,84 @@
|
|||
local cmp = {}
|
||||
|
||||
---@alias cmp.ConfirmBehavior "'insert'" | "'replace'"
|
||||
cmp.ConfirmBehavior = {}
|
||||
cmp.ConfirmBehavior.Insert = 'insert'
|
||||
cmp.ConfirmBehavior.Replace = 'replace'
|
||||
|
||||
---@alias cmp.ContextReason "'auto'" | "'manual'" | "'none'"
|
||||
cmp.ContextReason = {}
|
||||
cmp.ContextReason.Auto = 'auto'
|
||||
cmp.ContextReason.Manual = 'manual'
|
||||
cmp.ContextReason.None = 'none'
|
||||
|
||||
---@alias cmp.TriggerEvent "'InsertEnter'" | "'TextChanged'"
|
||||
cmp.TriggerEvent = {}
|
||||
cmp.TriggerEvent.InsertEnter = 'InsertEnter'
|
||||
cmp.TriggerEvent.TextChanged = 'TextChanged'
|
||||
|
||||
---@class cmp.ContextOption
|
||||
---@field public reason cmp.ContextReason|nil
|
||||
|
||||
---@class cmp.ConfirmOption
|
||||
---@field public behavior cmp.ConfirmBehavior
|
||||
|
||||
---@class cmp.SnippetExpansionParams
|
||||
---@field public body string
|
||||
---@field public insert_text_mode number
|
||||
|
||||
---@class cmp.Setup
|
||||
---@field public __call fun(c: cmp.ConfigSchema)
|
||||
---@field public buffer fun(c: cmp.ConfigSchema)
|
||||
---@field public global fun(c: cmp.ConfigSchema)
|
||||
|
||||
---@class cmp.CompletionRequest
|
||||
---@field public context cmp.Context
|
||||
---@field public option table
|
||||
---@field public offset number
|
||||
---@field public completion_context lsp.CompletionContext
|
||||
|
||||
---@class cmp.ConfigSchema
|
||||
---@field private revision number
|
||||
---@field public completion cmp.CompletionConfig
|
||||
---@field public documentation cmp.DocumentationConfig
|
||||
---@field public confirmation cmp.ConfirmationConfig
|
||||
---@field public sorting cmp.SortingConfig
|
||||
---@field public formatting cmp.FormattingConfig
|
||||
---@field public snippet cmp.SnippetConfig
|
||||
---@field public sources cmp.SourceConfig[]
|
||||
|
||||
---@class cmp.CompletionConfig
|
||||
---@field public autocomplete cmp.TriggerEvent[]
|
||||
---@field public completeopt string
|
||||
---@field public keyword_pattern string
|
||||
---@field public keyword_length number
|
||||
|
||||
---@class cmp.DocumentationConfig
|
||||
---@field public border string[]
|
||||
---@field public winhighlight string
|
||||
---@field public maxwidth number|nil
|
||||
---@field public maxheight number|nil
|
||||
|
||||
---@class cmp.ConfirmationConfig
|
||||
---@field public default_behavior cmp.ConfirmBehavior
|
||||
---@field public mapping table<string, cmp.ConfirmMappingConfig>
|
||||
|
||||
---@class cmp.ConfirmMappingConfig
|
||||
---@field behavior cmp.ConfirmBehavior
|
||||
---@field select boolean
|
||||
|
||||
---@class cmp.SortingConfig
|
||||
---@field public sort fun(entries: cmp.Entry[]): cmp.Entry[]
|
||||
|
||||
---@class cmp.FormattingConfig
|
||||
---@field public format fun(entry: cmp.Entry, suggeset_offset: number): vim.CompletedItem
|
||||
|
||||
---@class cmp.SnippetConfig
|
||||
---@field public expand fun(args: cmp.SnippetExpansionParams)
|
||||
|
||||
---@class cmp.SourceConfig
|
||||
---@field public name string
|
||||
---@field public opts table
|
||||
|
||||
return cmp
|
||||
|
8
lua/cmp/types/init.lua
Normal file
8
lua/cmp/types/init.lua
Normal file
|
@ -0,0 +1,8 @@
|
|||
local types = {}
|
||||
|
||||
types.cmp = require('cmp.types.cmp')
|
||||
types.lsp = require('cmp.types.lsp')
|
||||
types.vim = require('cmp.types.vim')
|
||||
|
||||
return types
|
||||
|
205
lua/cmp/types/lsp.lua
Normal file
205
lua/cmp/types/lsp.lua
Normal file
|
@ -0,0 +1,205 @@
|
|||
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/
|
||||
---@class lsp
|
||||
local lsp = {}
|
||||
|
||||
lsp.Position = {}
|
||||
|
||||
---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
|
||||
for i = position.character, 1, -1 do
|
||||
local s, v = pcall(function()
|
||||
return {
|
||||
row = position.line + 1,
|
||||
col = vim.str_byteindex(lines[1], i) + 1
|
||||
}
|
||||
end)
|
||||
if s then
|
||||
return v
|
||||
end
|
||||
end
|
||||
end
|
||||
return {
|
||||
row = position.line + 1,
|
||||
col = position.character + 1,
|
||||
}
|
||||
end
|
||||
|
||||
---Convert lsp.Position to vim.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
|
||||
return {
|
||||
line = position.row - 1,
|
||||
character = vim.str_utfindex(lines[1], math.max(0, math.min(position.col - 1, #lines[1]))),
|
||||
}
|
||||
end
|
||||
return {
|
||||
line = position.row - 1,
|
||||
character = position.col - 1,
|
||||
}
|
||||
end
|
||||
|
||||
lsp.Range = {}
|
||||
|
||||
---Convert lsp.Position to vim.Position
|
||||
---@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
|
||||
|
||||
---Convert lsp.Position to vim.Position
|
||||
---@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
|
||||
|
||||
---@alias lsp.CompletionTriggerKind "1" | "2" | "3"
|
||||
lsp.CompletionTriggerKind = {}
|
||||
lsp.CompletionTriggerKind.Invoked = 1
|
||||
lsp.CompletionTriggerKind.TriggerCharacter = 2
|
||||
lsp.CompletionTriggerKind.TriggerForIncompleteCompletions = 3
|
||||
|
||||
---@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.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 items lsp.CompletionItem[]
|
||||
|
||||
---@alias lsp.CompletionResponse lsp.CompletionList|lsp.CompletionItem[]|nil
|
||||
|
||||
---@class lsp.MarkupContent
|
||||
---@field public kind lsp.MarkupKind
|
||||
---@field public value string
|
||||
|
||||
---@class lsp.Position
|
||||
---@field public line number
|
||||
---@field public character number
|
||||
|
||||
---@class lsp.Range
|
||||
---@field public start lsp.Position
|
||||
---@field public end lsp.Position
|
||||
|
||||
---@class lsp.Command
|
||||
---@field public title string
|
||||
---@field public command string
|
||||
---@field public arguments any[]|nil
|
||||
|
||||
---@class lsp.TextEdit
|
||||
---@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
|
||||
---@field public newText string
|
||||
|
||||
---@class lsp.CompletionItemLabelDetails
|
||||
---@field public parameters string|nil
|
||||
---@field public qualifier string|nil
|
||||
---@field public type string|nil
|
||||
|
||||
---@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
|
||||
---
|
||||
---TODO: Should send the issue for upstream?
|
||||
---@field public word string|nil
|
||||
---@field public dup boolean|nil
|
||||
|
||||
return lsp
|
||||
|
47
lua/cmp/types/lsp_spec.lua
Normal file
47
lua/cmp/types/lsp_spec.lua
Normal file
|
@ -0,0 +1,47 @@
|
|||
local spec = require'cmp.utils.spec'
|
||||
local lsp = require'cmp.types.lsp'
|
||||
|
||||
describe('types.lsp', function ()
|
||||
before_each(spec.before)
|
||||
describe('Position', function ()
|
||||
vim.fn.setline('1', {
|
||||
'あいうえお',
|
||||
'かきくけこ',
|
||||
'さしすせそ',
|
||||
})
|
||||
local vim_position, lsp_position
|
||||
|
||||
vim_position = lsp.Position.to_vim('%', { line = 1, character = 3 })
|
||||
assert.are.equal(vim_position.row, 2)
|
||||
assert.are.equal(vim_position.col, 10)
|
||||
lsp_position = lsp.Position.to_lsp('%', vim_position)
|
||||
assert.are.equal(lsp_position.line, 1)
|
||||
assert.are.equal(lsp_position.character, 3)
|
||||
|
||||
vim_position = lsp.Position.to_vim('%', { line = 1, character = 0 })
|
||||
assert.are.equal(vim_position.row, 2)
|
||||
assert.are.equal(vim_position.col, 1)
|
||||
lsp_position = lsp.Position.to_lsp('%', vim_position)
|
||||
assert.are.equal(lsp_position.line, 1)
|
||||
assert.are.equal(lsp_position.character, 0)
|
||||
|
||||
vim_position = lsp.Position.to_vim('%', { line = 1, character = 5 })
|
||||
assert.are.equal(vim_position.row, 2)
|
||||
assert.are.equal(vim_position.col, 16)
|
||||
lsp_position = lsp.Position.to_lsp('%', vim_position)
|
||||
assert.are.equal(lsp_position.line, 1)
|
||||
assert.are.equal(lsp_position.character, 5)
|
||||
|
||||
-- overflow (lsp -> vim)
|
||||
vim_position = lsp.Position.to_vim('%', { line = 1, character = 6 })
|
||||
assert.are.equal(vim_position.row, 2)
|
||||
assert.are.equal(vim_position.col, 16)
|
||||
|
||||
-- overflow(vim -> lsp)
|
||||
vim_position.col = vim_position.col + 1
|
||||
lsp_position = lsp.Position.to_lsp('%', vim_position)
|
||||
assert.are.equal(lsp_position.line, 1)
|
||||
assert.are.equal(lsp_position.character, 5)
|
||||
end)
|
||||
end)
|
||||
|
18
lua/cmp/types/vim.lua
Normal file
18
lua/cmp/types/vim.lua
Normal file
|
@ -0,0 +1,18 @@
|
|||
---@class vim.CompletedItem
|
||||
---@field public word string
|
||||
---@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 id any
|
||||
|
||||
---@class vim.Position
|
||||
---@field public row number
|
||||
---@field public col number
|
||||
|
||||
---@class vim.Range
|
||||
---@field public start vim.Position
|
||||
---@field public end vim.Position
|
||||
|
55
lua/cmp/utils/async.lua
Normal file
55
lua/cmp/utils/async.lua
Normal file
|
@ -0,0 +1,55 @@
|
|||
local async = {}
|
||||
|
||||
---@class cmp.AsyncThrottle
|
||||
---@field public timeout number
|
||||
---@field public stop function
|
||||
---@field public __call function
|
||||
|
||||
---@param fn function
|
||||
---@param timeout number
|
||||
---@return cmp.AsyncThrottle
|
||||
async.throttle = function(fn, timeout)
|
||||
local time = nil
|
||||
local timer = vim.loop.new_timer()
|
||||
return setmetatable({
|
||||
timeout = timeout,
|
||||
stop = function()
|
||||
time = nil
|
||||
timer:stop()
|
||||
end,
|
||||
}, {
|
||||
__call = function(self, ...)
|
||||
local args = { ... }
|
||||
|
||||
if time == nil then
|
||||
time = vim.loop.now()
|
||||
end
|
||||
timer:stop()
|
||||
|
||||
local delta = math.max(0, self.timeout - (vim.loop.now() - time))
|
||||
timer:start(delta, 0, vim.schedule_wrap(function()
|
||||
time = nil
|
||||
fn(unpack(args))
|
||||
end))
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
---Create deduplicated callback
|
||||
---@return function
|
||||
async.dedup = function()
|
||||
local id = 0
|
||||
return function(callback)
|
||||
id = id + 1
|
||||
|
||||
local current = id
|
||||
return function(...)
|
||||
if current == id then
|
||||
callback(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return async
|
||||
|
40
lua/cmp/utils/async_spec.lua
Normal file
40
lua/cmp/utils/async_spec.lua
Normal file
|
@ -0,0 +1,40 @@
|
|||
local async = require "cmp.utils.async"
|
||||
|
||||
describe('utils.async', function()
|
||||
|
||||
it('throttle', function()
|
||||
local count = 0
|
||||
local now
|
||||
local f = async.throttle(function()
|
||||
count = count + 1
|
||||
end, 100)
|
||||
|
||||
-- 1. delay for 100ms
|
||||
now = vim.loop.now()
|
||||
f.timeout = 100
|
||||
f()
|
||||
vim.wait(1000, function() return count == 1 end)
|
||||
assert.is.truthy(math.abs(f.timeout - (vim.loop.now() - now)) < 10)
|
||||
|
||||
-- 2. delay for 500ms
|
||||
now = vim.loop.now()
|
||||
f.timeout = 500
|
||||
f()
|
||||
vim.wait(1000, function() return count == 2 end)
|
||||
assert.is.truthy(math.abs(f.timeout - (vim.loop.now() - now)) < 10)
|
||||
|
||||
-- 4. delay for 500ms and wait 100ms (remain 400ms)
|
||||
f.timeout = 500
|
||||
f()
|
||||
vim.wait(100) -- remain 400ms
|
||||
|
||||
-- 5. call immediately (100ms already elapsed from No.4)
|
||||
now = vim.loop.now()
|
||||
f.timeout = 100
|
||||
f()
|
||||
vim.wait(1000, function() return count == 3 end)
|
||||
assert.is.truthy(math.abs(vim.loop.now() - now) < 10)
|
||||
end)
|
||||
|
||||
end)
|
||||
|
57
lua/cmp/utils/cache.lua
Normal file
57
lua/cmp/utils/cache.lua
Normal file
|
@ -0,0 +1,57 @@
|
|||
---@class cmp.Cache
|
||||
---@field public entries any
|
||||
local cache = {}
|
||||
|
||||
cache.new = function()
|
||||
local self = setmetatable({}, { __index = cache })
|
||||
self.entries = {}
|
||||
return self
|
||||
end
|
||||
|
||||
---Get cache value
|
||||
---@param key string
|
||||
---@return any|nil
|
||||
cache.get = function(self, key)
|
||||
key = self:key(key)
|
||||
if self.entries[key] ~= nil then
|
||||
return unpack(self.entries[key])
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---Set cache value explicitly
|
||||
---@param key string
|
||||
---@vararg any
|
||||
cache.set = function(self, key, ...)
|
||||
key = self:key(key)
|
||||
self.entries[key] = { ... }
|
||||
end
|
||||
|
||||
---Ensure value by callback
|
||||
---@param key string
|
||||
---@param callback fun(): any
|
||||
cache.ensure = function(self, key, callback)
|
||||
local value = self:get(key)
|
||||
if value == nil then
|
||||
self:set(key, callback())
|
||||
end
|
||||
return self:get(key)
|
||||
end
|
||||
|
||||
---Clear all cache entries
|
||||
cache.clear = function(self)
|
||||
self.entries = {}
|
||||
end
|
||||
|
||||
---Create key
|
||||
---@param key string|table
|
||||
---@return string
|
||||
cache.key = function(_, key)
|
||||
if type(key) == 'table' then
|
||||
return table.concat(key, ':')
|
||||
end
|
||||
return key
|
||||
end
|
||||
|
||||
return cache
|
||||
|
0
lua/cmp/utils/cache_spec.lua
Normal file
0
lua/cmp/utils/cache_spec.lua
Normal file
113
lua/cmp/utils/char.lua
Normal file
113
lua/cmp/utils/char.lua
Normal file
|
@ -0,0 +1,113 @@
|
|||
local alpha = {}
|
||||
string.gsub('abcdefghijklmnopqrstuvwxyz', '.', function(char)
|
||||
alpha[string.byte(char)] = true
|
||||
end)
|
||||
|
||||
local ALPHA = {}
|
||||
string.gsub('ABCDEFGHIJKLMNOPQRSTUVWXYZ', '.', function(char)
|
||||
ALPHA[string.byte(char)] = true
|
||||
end)
|
||||
|
||||
local digit = {}
|
||||
string.gsub('1234567890', '.', function(char)
|
||||
digit[string.byte(char)] = true
|
||||
end)
|
||||
|
||||
local white = {}
|
||||
string.gsub(' \t\n', '.', function(char)
|
||||
white[string.byte(char)] = true
|
||||
end)
|
||||
|
||||
local char = {}
|
||||
|
||||
---@param byte number
|
||||
---@return boolean
|
||||
char.is_upper = function(byte)
|
||||
return ALPHA[byte]
|
||||
end
|
||||
|
||||
---@param byte number
|
||||
---@return boolean
|
||||
char.is_alpha = function(byte)
|
||||
return alpha[byte] or ALPHA[byte]
|
||||
end
|
||||
|
||||
---@param byte number
|
||||
---@return boolean
|
||||
char.is_digit = function(byte)
|
||||
return digit[byte]
|
||||
end
|
||||
|
||||
---@param byte number
|
||||
---@return boolean
|
||||
char.is_white = function(byte)
|
||||
return white[byte]
|
||||
end
|
||||
|
||||
---@param byte number
|
||||
---@return boolean
|
||||
char.is_symbol = function(byte)
|
||||
return not (char.is_alnum(byte) or char.is_white(byte))
|
||||
end
|
||||
|
||||
---@param byte number
|
||||
---@return boolean
|
||||
char.is_printable = function(byte)
|
||||
return string.match(string.char(byte), '^%c$') == nil
|
||||
end
|
||||
|
||||
---@param byte number
|
||||
---@return boolean
|
||||
char.is_alnum = function(byte)
|
||||
return char.is_alpha(byte) or char.is_digit(byte)
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@param index number
|
||||
---@return boolean
|
||||
char.is_semantic_index = function(text, index)
|
||||
if index <= 1 then
|
||||
return true
|
||||
end
|
||||
|
||||
local prev = string.byte(text, index - 1)
|
||||
local curr = string.byte(text, index)
|
||||
|
||||
if not char.is_upper(prev) and char.is_upper(curr) then
|
||||
return true
|
||||
end
|
||||
if char.is_symbol(curr) or char.is_white(curr) then
|
||||
return true
|
||||
end
|
||||
if not char.is_alpha(prev) and char.is_alpha(curr) then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@param current_index number
|
||||
---@return boolean
|
||||
char.get_next_semantic_index = function(text, current_index)
|
||||
for i = current_index + 1, #text do
|
||||
if char.is_semantic_index(text, i) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return #text + 1
|
||||
end
|
||||
|
||||
---Ignore case match
|
||||
---@param byte1 number
|
||||
---@param byte2 number
|
||||
---@return boolean
|
||||
char.match = function(byte1, byte2)
|
||||
if not char.is_alpha(byte1) or not char.is_alpha(byte2) then
|
||||
return byte1 == byte2
|
||||
end
|
||||
local diff = byte1 - byte2
|
||||
return diff == 0 or diff == 32 or diff == -32
|
||||
end
|
||||
|
||||
return char
|
||||
|
20
lua/cmp/utils/debug.lua
Normal file
20
lua/cmp/utils/debug.lua
Normal file
|
@ -0,0 +1,20 @@
|
|||
local debug = {}
|
||||
|
||||
local flag = false
|
||||
|
||||
---Print log
|
||||
---@vararg any
|
||||
debug.log = function(...)
|
||||
if flag then
|
||||
local data = {}
|
||||
for _, v in ipairs({ ... }) do
|
||||
if not vim.tbl_contains({ 'string', 'number', 'boolean' }, type(v)) then
|
||||
v = vim.inspect(v)
|
||||
end
|
||||
table.insert(data, v)
|
||||
end
|
||||
print(table.concat(data, '\t'))
|
||||
end
|
||||
end
|
||||
|
||||
return debug
|
134
lua/cmp/utils/keymap.lua
Normal file
134
lua/cmp/utils/keymap.lua
Normal file
|
@ -0,0 +1,134 @@
|
|||
local misc = require('cmp.utils.misc')
|
||||
local cache = require('cmp.utils.cache')
|
||||
|
||||
local keymap = {}
|
||||
|
||||
---The mapping of vim notation and chars.
|
||||
keymap._table = {
|
||||
['<CR>'] = { '\n', '\r', '\r\n' },
|
||||
['<Tab>'] = { '\t' },
|
||||
['<BSlash>'] = { '\\' },
|
||||
['<Bar>'] = { '|' },
|
||||
['<Space>'] = { ' ' },
|
||||
}
|
||||
|
||||
---Shortcut for nvim_replace_termcodes
|
||||
---@param keys string
|
||||
---@return string
|
||||
keymap.t = function(keys)
|
||||
return vim.api.nvim_replace_termcodes(keys, true, true, true)
|
||||
end
|
||||
|
||||
---Return vim notation keymapping (simple conversion).
|
||||
---@param s string
|
||||
---@return string
|
||||
keymap.to_keymap = function(s)
|
||||
return string.gsub(s, '.', function(c)
|
||||
for key, chars in pairs(keymap._table) do
|
||||
if vim.tbl_contains(chars, c) then
|
||||
return key
|
||||
end
|
||||
end
|
||||
return c
|
||||
end)
|
||||
end
|
||||
|
||||
---Feedkeys with callback
|
||||
keymap.feedkeys = setmetatable({
|
||||
callbacks = {},
|
||||
}, {
|
||||
__call = function(self, keys, mode, callback)
|
||||
vim.fn.feedkeys(keymap.t(keys), mode)
|
||||
|
||||
if callback then
|
||||
local current_mode = string.sub(vim.api.nvim_get_mode().mode, 1, 1)
|
||||
local id = misc.id('cmp.utils.keymap.feedkeys')
|
||||
local cb = ('<Plug>(cmp-utils-keymap-feedkeys:%s)'):format(id)
|
||||
self.callbacks[id] = function()
|
||||
callback()
|
||||
vim.api.nvim_buf_del_keymap(0, current_mode, cb)
|
||||
return keymap.t('<Ignore>')
|
||||
end
|
||||
vim.api.nvim_buf_set_keymap(0, current_mode, cb, ('v:lua.cmp.utils.keymap.feedkeys.expr(%s)'):format(id), {
|
||||
expr = true,
|
||||
nowait = true,
|
||||
silent = true,
|
||||
})
|
||||
vim.fn.feedkeys(keymap.t(cb), '')
|
||||
end
|
||||
end
|
||||
})
|
||||
misc.set(_G, { 'cmp', 'utils', 'keymap', 'feedkeys', 'expr' }, function(id)
|
||||
if keymap.feedkeys.callbacks[id] then
|
||||
keymap.feedkeys.callbacks[id]()
|
||||
end
|
||||
return keymap.t('<Ignore>')
|
||||
end)
|
||||
|
||||
---Register keypress handler.
|
||||
keymap.listen = setmetatable({
|
||||
cache = cache.new(),
|
||||
}, {
|
||||
__call = function(_, keys, callback)
|
||||
keys = keymap.to_keymap(keys)
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
if keymap.listen.cache:get({ bufnr, keys }) then
|
||||
return
|
||||
end
|
||||
|
||||
local existing = nil
|
||||
for _, map in ipairs(vim.api.nvim_buf_get_keymap(0, 'i')) do
|
||||
if existing then
|
||||
break
|
||||
end
|
||||
if map.lhs == keys then
|
||||
existing = map
|
||||
end
|
||||
end
|
||||
for _, map in ipairs(vim.api.nvim_get_keymap('i')) do
|
||||
if existing then
|
||||
break
|
||||
end
|
||||
if map.lhs == keys then
|
||||
existing = map
|
||||
break
|
||||
end
|
||||
end
|
||||
existing = existing or {
|
||||
lhs = keys,
|
||||
rhs = keys,
|
||||
expr = 0,
|
||||
nowait = 0,
|
||||
noremap = 1,
|
||||
}
|
||||
|
||||
keymap.listen.cache:set({ bufnr, keys }, {
|
||||
existing = existing,
|
||||
callback = callback,
|
||||
})
|
||||
vim.api.nvim_buf_set_keymap(0, 'i', keys, ('v:lua.cmp.utils.keymap.expr("%s")'):format(keys), {
|
||||
expr = true,
|
||||
nowait = true,
|
||||
noremap = true,
|
||||
})
|
||||
end,
|
||||
})
|
||||
misc.set(_G, { 'cmp', 'utils', 'keymap', 'expr' }, function(keys)
|
||||
keys = keymap.to_keymap(keys)
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
|
||||
local existing = keymap.listen.cache:get({ bufnr, keys }).existing
|
||||
local callback = keymap.listen.cache:get({ bufnr, keys }).callback
|
||||
callback(keys, function()
|
||||
vim.api.nvim_buf_set_keymap(0, 'i', '<Plug>(cmp-utils-keymap:_)', existing.rhs, {
|
||||
expr = existing.expr == 1,
|
||||
noremap = existing.noremap == 1,
|
||||
})
|
||||
vim.fn.feedkeys(keymap.t('<Plug>(cmp-utils-keymap:_)'), 'i')
|
||||
end)
|
||||
return keymap.t('<Ignore>')
|
||||
end)
|
||||
|
||||
return keymap
|
||||
|
13
lua/cmp/utils/keymap_spec.lua
Normal file
13
lua/cmp/utils/keymap_spec.lua
Normal file
|
@ -0,0 +1,13 @@
|
|||
local spec = require('cmp.utils.spec')
|
||||
|
||||
local keymap = require('cmp.utils.keymap')
|
||||
|
||||
describe('keymap', function()
|
||||
before_each(spec.before)
|
||||
|
||||
it('to_keymap', function()
|
||||
assert.are.equal(keymap.to_keymap('\n'), '<CR>')
|
||||
assert.are.equal(keymap.to_keymap('<CR>'), '<CR>')
|
||||
assert.are.equal(keymap.to_keymap('|'), '<Bar>')
|
||||
end)
|
||||
end)
|
112
lua/cmp/utils/misc.lua
Normal file
112
lua/cmp/utils/misc.lua
Normal file
|
@ -0,0 +1,112 @@
|
|||
local misc = {}
|
||||
|
||||
---Return concatenated list
|
||||
---@param list1 any[]
|
||||
---@param list2 any[]
|
||||
---@return any[]
|
||||
misc.concat = function(list1, list2)
|
||||
local new_list = {}
|
||||
for _, v in ipairs(list1) do
|
||||
table.insert(new_list, v)
|
||||
end
|
||||
for _, v in ipairs(list2) do
|
||||
table.insert(new_list, v)
|
||||
end
|
||||
return new_list
|
||||
end
|
||||
|
||||
---Merge two tables recursively
|
||||
---@generic T
|
||||
---@param v1 T
|
||||
---@param v2 T
|
||||
---@return T
|
||||
misc.merge = function(v1, v2)
|
||||
local merge1 = type(v1) == "table" and not vim.tbl_islist(v1)
|
||||
local merge2 = type(v2) == "table" and not vim.tbl_islist(v1)
|
||||
if merge1 and merge2 then
|
||||
local new_tbl = {}
|
||||
for k, v in pairs(v2) do
|
||||
new_tbl[k] = misc.merge(v1[k], v)
|
||||
end
|
||||
for k, v in pairs(v1) do
|
||||
if v2[k] == nil then
|
||||
new_tbl[k] = v
|
||||
end
|
||||
end
|
||||
return new_tbl
|
||||
end
|
||||
return v1 or v2
|
||||
end
|
||||
|
||||
|
||||
---Generate id for group name
|
||||
misc.id = setmetatable({
|
||||
group = {}
|
||||
}, {
|
||||
__call = function(_, group)
|
||||
misc.id.group[group] = misc.id.group[group] or 0
|
||||
misc.id.group[group] = misc.id.group[group] + 1
|
||||
return misc.id.group[group]
|
||||
end
|
||||
})
|
||||
|
||||
---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 def boolean
|
||||
---@return boolean
|
||||
misc.bool = function(v, def)
|
||||
if misc.safe(v) == nil then
|
||||
return def
|
||||
end
|
||||
return v == true or v == 1
|
||||
end
|
||||
|
||||
---Set value to deep object
|
||||
---@param t table
|
||||
---@param keys string[]
|
||||
---@param v any
|
||||
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 = c[key]
|
||||
end
|
||||
c[keys[#keys]] = v
|
||||
end
|
||||
|
||||
---Copy table
|
||||
---@generic T
|
||||
---@param tbl T
|
||||
---@return T
|
||||
misc.copy = function(tbl)
|
||||
if type(tbl) ~= 'table' then
|
||||
return tbl
|
||||
end
|
||||
|
||||
if vim.tbl_islist(tbl) then
|
||||
local copy = {}
|
||||
for i, value in ipairs(tbl) do
|
||||
copy[i] = misc.copy(value)
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
local copy = {}
|
||||
for key, value in pairs(tbl) do
|
||||
copy[key] = misc.copy(value)
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
return misc
|
32
lua/cmp/utils/patch.lua
Normal file
32
lua/cmp/utils/patch.lua
Normal file
|
@ -0,0 +1,32 @@
|
|||
local keymap = require('cmp.utils.keymap')
|
||||
local types = require('cmp.types')
|
||||
|
||||
local patch = {}
|
||||
|
||||
---@type table<number, function>
|
||||
patch.callbacks = {}
|
||||
|
||||
---Apply oneline textEdit
|
||||
---@param ctx cmp.Context
|
||||
---@param range lsp.Range
|
||||
---@param word string
|
||||
---@param callback function
|
||||
patch.apply = function(ctx, range, word, callback)
|
||||
local ok = true
|
||||
ok = ok and range.start.line == ctx.cursor.row - 1
|
||||
ok = ok and range.start.line == range['end'].line
|
||||
if not ok then
|
||||
error("text_edit's range must be current one line.")
|
||||
end
|
||||
range = types.lsp.Range.to_vim(ctx.bufnr, range)
|
||||
|
||||
local before = string.sub(ctx.cursor_before_line, range.start.col)
|
||||
local after = string.sub(ctx.cursor_after_line, ctx.cursor.col, range['end'].col)
|
||||
local before_len = vim.fn.strchars(before)
|
||||
local after_len = vim.fn.strchars(after)
|
||||
local keys = string.rep('<Left>', after_len) .. string.rep('<BS>', after_len + before_len) .. word
|
||||
keymap.feedkeys(keys, 'n', callback)
|
||||
end
|
||||
|
||||
return patch
|
||||
|
21
lua/cmp/utils/pattern.lua
Normal file
21
lua/cmp/utils/pattern.lua
Normal file
|
@ -0,0 +1,21 @@
|
|||
local pattern = {}
|
||||
|
||||
pattern._regexes = {}
|
||||
|
||||
pattern.regex = function(p)
|
||||
if not pattern._regexes[p] then
|
||||
pattern._regexes[p] = vim.regex(p)
|
||||
end
|
||||
return pattern._regexes[p]
|
||||
end
|
||||
|
||||
pattern.offset = function(p, text)
|
||||
local s, e = pattern.regex(p):match_str(text)
|
||||
if s then
|
||||
return s + 1, e + 1
|
||||
end
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
return pattern
|
||||
|
42
lua/cmp/utils/spec.lua
Normal file
42
lua/cmp/utils/spec.lua
Normal file
|
@ -0,0 +1,42 @@
|
|||
local context = require'cmp.context'
|
||||
local source = require 'cmp.source'
|
||||
local types = require('cmp.types')
|
||||
|
||||
local spec = {}
|
||||
|
||||
spec.before = function()
|
||||
vim.cmd [[
|
||||
bdelete!
|
||||
enew!
|
||||
setlocal virtualedit=all
|
||||
]]
|
||||
end
|
||||
|
||||
spec.state = function(text, row, col)
|
||||
vim.fn.setline(1, text)
|
||||
vim.fn.cursor(row, col)
|
||||
local ctx = context.empty()
|
||||
local s = source.new('spec', {
|
||||
complete = function()
|
||||
end
|
||||
})
|
||||
return {
|
||||
context = function()
|
||||
return ctx
|
||||
end,
|
||||
source = function()
|
||||
return s
|
||||
end,
|
||||
press = function(char)
|
||||
vim.fn.feedkeys(('i%s'):format(char), 'nx')
|
||||
vim.fn.feedkeys(('l'):format(char), 'nx')
|
||||
ctx.prev_context = nil
|
||||
ctx = context.new(ctx, { reason = types.cmp.ContextReason.Manual })
|
||||
s:complete(ctx, function() end)
|
||||
return ctx
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
return spec
|
||||
|
150
lua/cmp/utils/str.lua
Normal file
150
lua/cmp/utils/str.lua
Normal file
|
@ -0,0 +1,150 @@
|
|||
local char = require'cmp.utils.char'
|
||||
local pattern = require 'cmp.utils.pattern'
|
||||
|
||||
local str = {}
|
||||
|
||||
local INVALID_CHARS = {}
|
||||
INVALID_CHARS[string.byte('=')] = true
|
||||
INVALID_CHARS[string.byte('$')] = true
|
||||
INVALID_CHARS[string.byte('(')] = true
|
||||
INVALID_CHARS[string.byte('[')] = true
|
||||
INVALID_CHARS[string.byte('"')] = true
|
||||
INVALID_CHARS[string.byte("'")] = true
|
||||
INVALID_CHARS[string.byte("\n")] = true
|
||||
|
||||
local PAIR_CHARS = {}
|
||||
PAIR_CHARS[string.byte('[')] = string.byte(']')
|
||||
PAIR_CHARS[string.byte('(')] = string.byte(')')
|
||||
PAIR_CHARS[string.byte('<')] = string.byte('>')
|
||||
|
||||
---Return if specified text has prefix or not
|
||||
---@param text string
|
||||
---@param prefix string
|
||||
---@return boolean
|
||||
str.has_prefix = function(text, prefix)
|
||||
if #text < #prefix then
|
||||
return false
|
||||
end
|
||||
for i = 1, #prefix do
|
||||
if not char.match(string.byte(text, i), string.byte(prefix, i)) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---Remove suffix
|
||||
---@param text string
|
||||
---@param suffix string
|
||||
---@return string
|
||||
str.remove_suffix = function(text, suffix)
|
||||
if #text < #suffix then
|
||||
return text
|
||||
end
|
||||
|
||||
local i = 0
|
||||
while i < #suffix do
|
||||
if string.byte(text, #text - i) ~= string.byte(suffix, #suffix - i) then
|
||||
return text
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
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
|
||||
|
||||
---omit
|
||||
---@param text string
|
||||
---@param width number
|
||||
---@return string
|
||||
str.omit = function(text, width)
|
||||
if width == 0 then
|
||||
return ''
|
||||
end
|
||||
|
||||
if not text then
|
||||
text = ''
|
||||
end
|
||||
if #text > width then
|
||||
return string.sub(text, 1, width + 1) .. '...'
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
---trim
|
||||
---@param text string
|
||||
---@return string
|
||||
str.trim = function(text)
|
||||
local s = 1
|
||||
for i = 1, #text do
|
||||
if not char.is_white(string.byte(text, i)) then
|
||||
s = i
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local e = #text
|
||||
for i = #text, 1, -1 do
|
||||
if not char.is_white(string.byte(text, i)) then
|
||||
e = i
|
||||
break
|
||||
end
|
||||
end
|
||||
if s == 1 and e == #text then
|
||||
return text
|
||||
end
|
||||
return string.sub(text, s, e)
|
||||
end
|
||||
|
||||
---get_word
|
||||
---@param text string
|
||||
---@return string
|
||||
str.get_word = function(text, stop_char)
|
||||
local valids = {}
|
||||
local has_valid = false
|
||||
for idx = 1, #text do
|
||||
local c = string.byte(text, idx)
|
||||
local invalid = INVALID_CHARS[c] and not (valids[c] and stop_char ~= c)
|
||||
if has_valid and invalid then
|
||||
return string.sub(text, 1, idx - 1)
|
||||
end
|
||||
valids[c] = true
|
||||
if PAIR_CHARS[c] then
|
||||
valids[PAIR_CHARS[c]] = true
|
||||
end
|
||||
has_valid = has_valid or not invalid
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
---Oneline
|
||||
---@param text string
|
||||
---@return string
|
||||
str.oneline = function(text)
|
||||
for i = 1, #text do
|
||||
if string.byte(text, i) == string.byte('\n', 1) then
|
||||
return string.sub(text, 1, i - 1)
|
||||
end
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
return str
|
||||
|
||||
|
26
lua/cmp/utils/str_spec.lua
Normal file
26
lua/cmp/utils/str_spec.lua
Normal file
|
@ -0,0 +1,26 @@
|
|||
local str = require "cmp.utils.str"
|
||||
|
||||
describe('utils.str', function()
|
||||
|
||||
it('get_word', function()
|
||||
assert.are.equal(str.get_word('print'), 'print')
|
||||
assert.are.equal(str.get_word('$variable'), '$variable')
|
||||
assert.are.equal(str.get_word('print()'), 'print')
|
||||
assert.are.equal(str.get_word('["cmp#confirm"]'), '["cmp#confirm"]')
|
||||
assert.are.equal(str.get_word('"devDependencies":', string.byte('"')), '"devDependencies')
|
||||
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()')
|
||||
assert.are.equal(str.remove_suffix('log()${0}', '${0}'), 'log()')
|
||||
assert.are.equal(str.remove_suffix('log()${0:placeholder}', '${0}'), 'log()${0:placeholder}')
|
||||
end)
|
||||
|
||||
end)
|
||||
|
||||
|
38
plugin/cmp.lua
Normal file
38
plugin/cmp.lua
Normal file
|
@ -0,0 +1,38 @@
|
|||
if vim.g.loaded_cmp then
|
||||
return
|
||||
end
|
||||
vim.g.loaded_cmp = true
|
||||
|
||||
local cmp = require'cmp'
|
||||
local misc = require'cmp.utils.misc'
|
||||
|
||||
-- TODO: https://github.com/neovim/neovim/pull/14661
|
||||
vim.cmd [[
|
||||
augroup cmp
|
||||
autocmd!
|
||||
autocmd InsertEnter * lua require'cmp.autocmd'.emit('InsertEnter')
|
||||
autocmd InsertLeave * lua require'cmp.autocmd'.emit('InsertLeave')
|
||||
autocmd TextChangedI,TextChangedP * lua require'cmp.autocmd'.emit('TextChanged')
|
||||
autocmd CompleteChanged * lua require'cmp.autocmd'.emit('CompleteChanged')
|
||||
autocmd CompleteDone * lua require'cmp.autocmd'.emit('CompleteDone')
|
||||
augroup END
|
||||
]]
|
||||
|
||||
if vim.fn.hlexists('CmpDocumentation') == 0 then
|
||||
vim.cmd [[highlight link CmpDocumentation NormalFloat]]
|
||||
end
|
||||
|
||||
if vim.fn.hlexists('CmpDocumentationBorder') == 0 then
|
||||
vim.cmd [[highlight link CmpDocumentationBorder NormalFloat]]
|
||||
end
|
||||
|
||||
misc.set(_G, { 'cmp', 'complete' }, function()
|
||||
cmp.complete()
|
||||
return vim.api.nvim_replace_termcodes('<Ignore>', true, true, true)
|
||||
end)
|
||||
|
||||
misc.set(_G, { 'cmp', 'close' }, function()
|
||||
cmp.close()
|
||||
return vim.api.nvim_replace_termcodes('<Ignore>', true, true, true)
|
||||
end)
|
||||
|
4
stylua.toml
Normal file
4
stylua.toml
Normal file
|
@ -0,0 +1,4 @@
|
|||
indent_type = "Spaces"
|
||||
indent_width = 2
|
||||
column_width = 1200
|
||||
quote_style = "AutoPreferSingle"
|
Loading…
Reference in a new issue