1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-24 05:20:04 +08:00
SpaceVim/bundle/echodoc.vim/autoload/echodoc/util.vim
2024-06-05 19:16:56 +08:00

336 lines
9.0 KiB
VimL

"=============================================================================
" FILE: autoload/echodoc/util.vim
" AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
" Tommy Allen <tommy@esdf.io>
" License: MIT license
"=============================================================================
" Return the next column and character at the column position in text. Keep
" scanning until a usable character is found. This should be safe for
" multi-byte characters.
function! s:mbprevchar(text, col) abort
if a:col < 0
return [a:col, '']
endif
let c = matchstr(a:text, '\%'.a:col.'c.')
let c1 = a:col - 1
while c1 > 0 && c ==# ''
let c = matchstr(a:text, '\%'.c1.'c.')
let c1 -= 1
endwhile
return [c1, c]
endfunction
" Try to find a function at the current position.
" Stops if `echodoc_max_blank_lines` is encountered. (max 50)
function! echodoc#util#get_func_text() abort
let l2 = line('.')
let c2 = col('.') - 1
let l1 = l2
let c1 = c2
let skip = 0
let last_quote = ''
let text = getline(l1)[: c2-1]
let found = 0
let line_guard = 0
let blank = 0
let max_blank = max([1, get(b:, 'echodoc_max_blank_lines',
\ get(g:, 'echodoc_max_blank_lines', 1))])
let max_guard = get(b:, 'echodoc_max_line_guard',
\ get(g:, 'echodoc_max_line_guard', 5))
let check_quotes = s:check_quotes()
while l1 > 0 && line_guard < max_guard && blank < max_blank
if c1 <= 0
let l1 -= 1
let c1 = col([l1, '$'])
let text = getline(l1)
if text ==# ''
let blank += 1
else
let blank = 0
endif
let line_guard += 1
" Clear last quote
let last_quote = ''
continue
endif
if len(text) >= &l:columns
break
endif
let [c1, c] = s:mbprevchar(text, c1)
let p = ''
if c1 > 0
let [_, p] = s:mbprevchar(text, c1)
if p ==# '\'
continue
endif
endif
if c =~# '\s'
" Skip
continue
endif
if last_quote ==# '' && index(check_quotes, c) >= 0
let last_quote = c
continue
elseif last_quote !=# ''
if last_quote == c
let last_quote = ''
endif
continue
endif
if c ==# '('
if skip == 0
if p =~# '\k\|\s'
let found = 1
break
endif
else
let skip -= 1
endif
elseif c ==# ')'
let skip += 1
endif
endwhile
if (found || last_quote !=# '') && l1 > 0 && c1 > 0
let lines = getline(l1, l2)
let lines[-1] = c2 == 0 ? '' : lines[-1][:c2 - 1]
let lines[0] = c1 == 0 ? '' : matchstr(lines[0], '\k\+\%>'.(c1 - 1).'c.*')
return [l1, c1, join(lines, "\n")]
endif
return [-1, -1, '']
endfunction
" Returns a parsed stack of functions found in the text. Each item in the
" stack contains a dict:
" - name: Function name.
" - start: Argument start position.
" - end: Argument end position. -1 if the function is unclosed.
" - pos: The argument position. 1-indexed, 0 = no args, -1 = closed.
" - ppos: The function's position in the previous function in the stack.
" - args: A list of arguments.
function! echodoc#util#parse_funcs(text, filetype) abort
if a:text ==# ''
return []
endif
let text = a:text
" Function pointer pattern.
" Example: int32_t get(void *const, const size_t)
let text = substitute(text, '\s*(\*)\s*', '', 'g')
let text = substitute(text, '^(\(.*\))$', '\1', '')
let text = substitute(text, '\s\+\ze(', '', '')
" Template arguments pattern.
let text = substitute(text, '<\zs[^(]*\ze>', '...', 'g')
let quote_i = -1
let stack = []
let open_stack = []
let comma = 0
" Matching pairs will count as a single argument entry so that commas can be
" skipped within them. The open depth is tracked in each open stack item.
" Parenthesis is an exception since it's used for functions and can have a
" depth of 1.
let pairs = '({[)}]'
let l = len(text) - 1
let i = -1
let check_quotes = s:check_quotes()
while i < l
let i += 1
let c = text[i]
if i > 0 && text[i - 1] ==# '\'
continue
endif
if quote_i != -1
" For languages that allow '''' ?
" if c == "'" && text[i - 1] == c && i - quote_i > 1
" continue
" endif
if c == text[quote_i]
let quote_i = -1
endif
continue
endif
if quote_i == -1 && index(check_quotes, c) >= 0
" backtick (`) is not used alone in languages that I know of.
let quote_i = i
continue
endif
let prev = len(open_stack) ? open_stack[-1] : {'opens': [0, 0, 0]}
let opened = prev.opens[0] + prev.opens[1] + prev.opens[2]
let p = stridx(pairs, c)
if p != -1
let ci = p % 3
if p == 3 && opened == 1 && prev.opens[0] == 1
" Closing the function parenthesis
if !empty(open_stack)
let item = remove(open_stack, -1)
let item.end = i - 1
let item.pos = -1
let item.opens[0] -= 1
if comma <= i
call add(item.args, text[comma :i - 1])
endif
let comma = item.i
endif
elseif p == 0
" Opening parenthesis
let func_i = match(text[:i - 1], '\S', comma)
let func_name = matchstr(substitute(text[func_i :i - 1],
\ '<[^>]*>', '', 'g'), '\k\+$')
if func_i != -1 && func_i < i && func_name !=# ''
let ppos = 0
if !empty(open_stack)
let ppos = open_stack[-1].pos
endif
if func_name !=# ''
" Opening parenthesis that's preceded by a non-empty string.
call add(stack, {
\ 'name': func_name,
\ 'i': func_i,
\ 'start': i + 1,
\ 'end': -1,
\ 'pos': 0,
\ 'ppos': ppos,
\ 'args': [],
\ 'opens': [1, 0, 0]
\ })
call add(open_stack, stack[-1])
" Function opening parenthesis marks the beginning of arguments.
" let comma = i + 1
let comma = i + 1
endif
else
let prev.opens[0] += 1
endif
else
let prev.opens[ci] += p > 2 ? -1 : 1
endif
elseif opened == 1 && prev.opens[0] == 1 && c ==# ','
" Not nested in a pair.
if !empty(open_stack) && comma <= i
let open_stack[-1].pos += 1
call add(open_stack[-1].args, text[comma :i - 1])
endif
let comma = i + 1
endif
endwhile
if !empty(open_stack)
let item = open_stack[-1]
call add(item.args, text[comma :l])
let item.pos += 1
endif
if index(['python', 'rust'], a:filetype) >= 0
" Remove self argument
let expr = a:filetype ==# 'python' ?
\ "v:val !=# 'self'" :
\ "index(['self', '&self', '&mut self'], v:val) < 0"
for item in stack
call filter(item.args, expr)
endfor
if !empty(stack) && !empty(stack[-1].args)
" Remove heading spaces
let stack[-1].args[0] = substitute(stack[-1].args[0], '^\s\+', '', '')
endif
endif
if !empty(stack) && stack[-1].opens[0] == 0
let item = stack[-1]
let item.trailing = matchstr(text, '\s*\zs\p*', item.end + 2)
endif
return stack
endfunction
function! echodoc#util#completion_signature(completion, maxlen, filetype) abort
if empty(a:completion)
return {}
endif
let item = a:completion
let abbrs = []
if type(get(item, 'user_data', 0)) ==# v:t_string && item.user_data !=# ''
let user_data = {}
silent! let user_data = json_decode(item.user_data)
if type(user_data) == v:t_dict && has_key(user_data, 'signature')
call add(abbrs, user_data.signature)
endif
endif
if get(item, 'info', '') =~# '^.\+(.*)'
call add(abbrs, matchstr(item.info, '^\_s*\zs.*'))
endif
if get(item, 'kind', '') =~# '^.\+(.*)'
call add(abbrs, item.kind)
endif
if get(item, 'menu', '') =~# '^.\+(.*)'
call add(abbrs, item.menu)
endif
if get(item, 'abbr', '') =~# '^.\+(.*)'
call add(abbrs, item.abbr)
endif
if item.word =~# '^.\+(.*)' || get(item, 'kind', '') ==# 'f'
call add(abbrs, item.word)
endif
let stack = []
for info in abbrs
let info = info[:a:maxlen]
let stack = echodoc#util#parse_funcs(info, a:filetype)
if !empty(stack)
break
endif
endfor
if empty(stack)
return {}
endif
let comp = stack[0]
let word = matchstr(a:completion.word, '\k\+\ze[()]*')
if index(['go', 'rust'], a:filetype) >= 0 && word !=# '' && comp.name !=# word
" Note: It is for Rust and Go only.
" Completion 'word' is what actually completed, if the parsed name is
" different, it's probably because 'info' is an abstract function
" signature. .e.g in Go:
" completed: BoolVar(p *bool, name string, value bool, usage string)
" info: func(p *bool, name string, value bool, usage string)
let comp.name = word
endif
return comp
endfunction
function! s:check_quotes() abort
" Note: It is very ugly check...
" Rust contains quoting pattern like "fn from(s: &'s str) -> String"
return &filetype !=# 'rust' ? ['"', '`', "'"] : ['"', '`']
endfunction