mirror of
https://github.com/prabirshrestha/vim-lsp.git
synced 2025-12-14 20:35:59 +01:00
* refactor to use lsp#utils#position#_lsp_to_vim * remove lsp#utils#to_col in favor of lsp#utils#position#_lsp_to_vim * fix doc
475 lines
16 KiB
VimL
475 lines
16 KiB
VimL
" vint: -ProhibitUnusedVariable
|
|
|
|
" constants {{{
|
|
|
|
let s:default_completion_item_kinds = {
|
|
\ '1': 'text',
|
|
\ '2': 'method',
|
|
\ '3': 'function',
|
|
\ '4': 'constructor',
|
|
\ '5': 'field',
|
|
\ '6': 'variable',
|
|
\ '7': 'class',
|
|
\ '8': 'interface',
|
|
\ '9': 'module',
|
|
\ '10': 'property',
|
|
\ '11': 'unit',
|
|
\ '12': 'value',
|
|
\ '13': 'enum',
|
|
\ '14': 'keyword',
|
|
\ '15': 'snippet',
|
|
\ '16': 'color',
|
|
\ '17': 'file',
|
|
\ '18': 'reference',
|
|
\ '19': 'folder',
|
|
\ '20': 'enum member',
|
|
\ '21': 'constant',
|
|
\ '22': 'struct',
|
|
\ '23': 'event',
|
|
\ '24': 'operator',
|
|
\ '25': 'type parameter',
|
|
\ }
|
|
|
|
let s:completion_item_kinds = {}
|
|
|
|
let s:completion_status_success = 'success'
|
|
let s:completion_status_failed = 'failed'
|
|
let s:completion_status_pending = 'pending'
|
|
|
|
let s:is_user_data_support = has('patch-8.0.1493')
|
|
let s:user_data_key = 'vim-lsp/textEdit'
|
|
let s:user_data_additional_edits_key = 'vim-lsp/additionalTextEdits'
|
|
let s:user_data_insert_start_key = 'vim-lsp/insertStart'
|
|
let s:user_data_insert_format_key = 'vim-lsp/insertFormat'
|
|
let s:user_data_filtertext_key = 'vim-lsp/filterText'
|
|
|
|
" }}}
|
|
|
|
" completion state
|
|
let s:completion = {'counter': 0, 'status': '', 'matches': []}
|
|
|
|
function! lsp#omni#complete(findstart, base) abort
|
|
let l:info = s:find_complete_servers()
|
|
if empty(l:info['server_names'])
|
|
return a:findstart ? -1 : []
|
|
endif
|
|
|
|
if a:findstart
|
|
return col('.')
|
|
else
|
|
if !g:lsp_async_completion
|
|
let s:completion['status'] = s:completion_status_pending
|
|
endif
|
|
|
|
call s:send_completion_request(l:info)
|
|
|
|
if g:lsp_async_completion
|
|
" automatically call `s:display_completions` at `s:handle_omnicompletion` when retrieved textDocument/completion response.
|
|
redraw
|
|
return exists('v:none') ? v:none : []
|
|
else
|
|
" wait for retrieve textDocument/completion response and then call `s:display_completions` explicitly.
|
|
while s:completion['status'] is# s:completion_status_pending && !complete_check()
|
|
sleep 10m
|
|
endwhile
|
|
call timer_start(0, { timer -> s:display_completions(timer, l:info) })
|
|
|
|
return exists('v:none') ? v:none : []
|
|
endif
|
|
endif
|
|
endfunction
|
|
|
|
function! s:get_insertion_point(item, current_line, typed_pattern) abort
|
|
if !has_key(a:item, 'user_data')
|
|
let l:insert_start = -1
|
|
else
|
|
let l:insert_start = get(json_decode(a:item['user_data']), s:user_data_insert_start_key, -1)
|
|
endif
|
|
|
|
if l:insert_start >= 0
|
|
return l:insert_start
|
|
else
|
|
return match(a:current_line, a:typed_pattern)
|
|
endif
|
|
endfunction
|
|
|
|
function! s:get_filter_label(item) abort
|
|
if !has_key(a:item, 'user_data')
|
|
return trim(a:item['word'])
|
|
endif
|
|
|
|
let l:user_data = json_decode(a:item['user_data'])
|
|
return trim(get(l:user_data, s:user_data_filtertext_key, a:item['word']))
|
|
endfunction
|
|
|
|
function! s:prefix_filter(item, last_typed_word) abort
|
|
let l:label = s:get_filter_label(a:item)
|
|
|
|
if g:lsp_ignorecase
|
|
return stridx(tolower(l:label), tolower(a:last_typed_word)) == 0
|
|
else
|
|
return stridx(l:label, a:last_typed_word) == 0
|
|
endif
|
|
endfunction
|
|
|
|
function! s:contains_filter(item, last_typed_word) abort
|
|
let l:label = s:get_filter_label(a:item)
|
|
|
|
if g:lsp_ignorecase
|
|
return stridx(tolower(l:label), tolower(a:last_typed_word)) >= 0
|
|
else
|
|
return stridx(l:label, a:last_typed_word) >= 0
|
|
endif
|
|
endfunction
|
|
|
|
function! s:display_completions(timer, info) abort
|
|
" TODO: Allow multiple servers
|
|
let l:server_name = a:info['server_names'][0]
|
|
let l:server_info = lsp#get_server_info(l:server_name)
|
|
|
|
let l:typed_pattern = has_key(l:server_info, 'config') && has_key(l:server_info['config'], 'typed_pattern') ? l:server_info['config']['typed_pattern'] : '\k*$'
|
|
let l:current_line = strpart(getline('.'), 0, col('.') - 1)
|
|
|
|
let s:start_pos = min(map(copy(s:completion['matches']), {_, item -> s:get_insertion_point(item, l:current_line, l:typed_pattern) }))
|
|
|
|
let l:filter = has_key(l:server_info, 'config') && has_key(l:server_info['config'], 'filter') ? l:server_info['config']['filter'] : { 'name': 'none' }
|
|
let l:last_typed_word = strpart(l:current_line, s:start_pos)
|
|
|
|
if l:filter['name'] ==? 'prefix'
|
|
let s:completion['matches'] = filter(s:completion['matches'], {_, item -> s:prefix_filter(item, l:last_typed_word)})
|
|
elseif l:filter['name'] ==? 'contains'
|
|
let s:completion['matches'] = filter(s:completion['matches'], {_, item -> s:contains_filter(item, l:last_typed_word)})
|
|
endif
|
|
|
|
let s:completion['status'] = ''
|
|
|
|
if mode() is# 'i'
|
|
call complete(s:start_pos + 1, s:completion['matches'])
|
|
endif
|
|
endfunction
|
|
|
|
function! s:handle_omnicompletion(server_name, complete_counter, info, data) abort
|
|
if s:completion['counter'] != a:complete_counter
|
|
" ignore old completion results
|
|
return
|
|
endif
|
|
|
|
if lsp#client#is_error(a:data) || !has_key(a:data, 'response') || !has_key(a:data['response'], 'result')
|
|
let s:completion['status'] = s:completion_status_failed
|
|
return
|
|
endif
|
|
|
|
let l:result = s:get_completion_result(a:server_name, a:data)
|
|
let l:matches = l:result['matches']
|
|
let s:completion['matches'] = l:matches
|
|
let s:completion['status'] = s:completion_status_success
|
|
|
|
if g:lsp_async_completion
|
|
call s:display_completions(0, a:info)
|
|
endif
|
|
endfunction
|
|
|
|
function! lsp#omni#get_kind_text(completion_item, ...) abort
|
|
let l:server = get(a:, 1, '')
|
|
if empty(l:server) " server name
|
|
let l:completion_item_kinds = s:default_completion_item_kinds
|
|
else
|
|
if !has_key(s:completion_item_kinds, l:server)
|
|
let l:server_info = lsp#get_server_info(l:server)
|
|
if has_key (l:server_info, 'config') && has_key(l:server_info['config'], 'completion_item_kinds')
|
|
let s:completion_item_kinds[l:server] = extend(copy(s:default_completion_item_kinds), l:server_info['config']['completion_item_kinds'])
|
|
else
|
|
let s:completion_item_kinds[l:server] = s:default_completion_item_kinds
|
|
endif
|
|
endif
|
|
let l:completion_item_kinds = s:completion_item_kinds[l:server]
|
|
endif
|
|
|
|
return has_key(a:completion_item, 'kind') && has_key(l:completion_item_kinds, a:completion_item['kind'])
|
|
\ ? l:completion_item_kinds[a:completion_item['kind']] : ''
|
|
endfunction
|
|
|
|
" auxiliary functions {{{
|
|
|
|
function! s:find_complete_servers() abort
|
|
let l:server_names = []
|
|
for l:server_name in lsp#get_whitelisted_servers()
|
|
let l:init_capabilities = lsp#get_server_capabilities(l:server_name)
|
|
if has_key(l:init_capabilities, 'completionProvider')
|
|
" TODO: support triggerCharacters
|
|
call add(l:server_names, l:server_name)
|
|
endif
|
|
endfor
|
|
|
|
return { 'server_names': l:server_names }
|
|
endfunction
|
|
|
|
function! s:send_completion_request(info) abort
|
|
let s:completion['counter'] = s:completion['counter'] + 1
|
|
let l:server_name = a:info['server_names'][0]
|
|
" TODO: support multiple servers
|
|
call lsp#send_request(l:server_name, {
|
|
\ 'method': 'textDocument/completion',
|
|
\ 'params': {
|
|
\ 'textDocument': lsp#get_text_document_identifier(),
|
|
\ 'position': lsp#get_position(),
|
|
\ },
|
|
\ 'on_notification': function('s:handle_omnicompletion', [l:server_name, s:completion['counter'], a:info]),
|
|
\ })
|
|
endfunction
|
|
|
|
function! s:get_completion_result(server_name, data) abort
|
|
let l:result = a:data['response']['result']
|
|
|
|
if type(l:result) == type([])
|
|
let l:items = l:result
|
|
let l:incomplete = 0
|
|
elseif type(l:result) == type({})
|
|
let l:items = l:result['items']
|
|
let l:incomplete = l:result['isIncomplete']
|
|
else
|
|
let l:items = []
|
|
let l:incomplete = 0
|
|
endif
|
|
|
|
let l:matches = type(l:items) == type([]) ? map(l:items, {_, item -> lsp#omni#get_vim_completion_item(item, a:server_name) }) : []
|
|
|
|
return {'matches': l:matches, 'incomplete': l:incomplete}
|
|
endfunction
|
|
|
|
function! lsp#omni#default_get_vim_completion_item(item, ...) abort
|
|
let l:server_name = get(a:, 1, '')
|
|
|
|
if g:lsp_insert_text_enabled && has_key(a:item, 'insertText') && !empty(a:item['insertText'])
|
|
if has_key(a:item, 'insertTextFormat') && a:item['insertTextFormat'] != 1
|
|
let l:word = a:item['label']
|
|
else
|
|
let l:word = a:item['insertText']
|
|
endif
|
|
let l:abbr = a:item['label']
|
|
else
|
|
let l:word = a:item['label']
|
|
let l:abbr = a:item['label']
|
|
endif
|
|
|
|
if has_key(a:item, 'insertTextFormat') && a:item['insertTextFormat'] == 2
|
|
let l:word = substitute(l:word, '\<\$[0-9]\+\|\${[^}]\+}\>', '', 'g')
|
|
endif
|
|
|
|
let l:kind = lsp#omni#get_kind_text(a:item, l:server_name)
|
|
|
|
let l:completion = {
|
|
\ 'word': l:word,
|
|
\ 'abbr': l:abbr,
|
|
\ 'menu': '',
|
|
\ 'info': '',
|
|
\ 'icase': 1,
|
|
\ 'dup': 1,
|
|
\ 'empty': 1,
|
|
\ 'kind': l:kind}
|
|
|
|
" check support user_data.
|
|
" if not support but g:lsp_text_edit_enabled enabled,
|
|
" then print information to user and add information to log file.
|
|
if !s:is_user_data_support && g:lsp_text_edit_enabled
|
|
let l:no_support_error_message = 'textEdit support on omni complete requires Vim 8.0 patch 1493 or later(please check g:lsp_text_edit_enabled)'
|
|
call lsp#utils#error(l:no_support_error_message)
|
|
call lsp#log(l:no_support_error_message)
|
|
endif
|
|
|
|
let l:user_data = {}
|
|
|
|
" Use '-1' to signal "no specific insertion point" set.
|
|
let l:user_data[s:user_data_insert_start_key] = -1
|
|
|
|
" add user_data in completion item, when
|
|
" 1. provided user_data
|
|
" 2. provided textEdit or additionalTextEdits
|
|
" 3. textEdit value is Dictionary or additionalTextEdits is non-empty list
|
|
if g:lsp_text_edit_enabled
|
|
let l:text_edit = get(a:item, 'textEdit', v:null)
|
|
let l:additional_text_edits = get(a:item, 'additionalTextEdits', v:null)
|
|
|
|
" type check
|
|
if type(l:text_edit) == type({})
|
|
let l:user_data[s:user_data_key] = l:text_edit
|
|
let l:user_data[s:user_data_insert_start_key] = l:text_edit['range']['start']['character']
|
|
let l:user_data[s:user_data_insert_format_key] = get(a:item, 'insertTextFormat', 0)
|
|
endif
|
|
|
|
if type(l:additional_text_edits) == type([]) && !empty(l:additional_text_edits)
|
|
let l:user_data[s:user_data_additional_edits_key] = l:additional_text_edits
|
|
endif
|
|
endif
|
|
|
|
" Store filterText in user_data
|
|
if s:is_user_data_support && has_key(a:item, 'filterText')
|
|
let l:user_data[s:user_data_filtertext_key] = a:item['filterText']
|
|
endif
|
|
|
|
if !empty(l:user_data)
|
|
let l:completion['user_data'] = json_encode(l:user_data)
|
|
endif
|
|
|
|
if has_key(a:item, 'detail') && !empty(a:item['detail'])
|
|
let l:completion['menu'] = substitute(a:item['detail'], '[ \t\n\r]\+', ' ', 'g')
|
|
endif
|
|
|
|
if has_key(a:item, 'documentation')
|
|
if type(a:item['documentation']) == type('') " field is string
|
|
let l:completion['info'] .= a:item['documentation']
|
|
elseif type(a:item['documentation']) == type({}) &&
|
|
\ has_key(a:item['documentation'], 'value')
|
|
" field is MarkupContent (hopefully 'plaintext')
|
|
let l:completion['info'] .= a:item['documentation']['value']
|
|
endif
|
|
endif
|
|
|
|
return l:completion
|
|
endfunction
|
|
|
|
function! lsp#omni#get_vim_completion_item(...) abort
|
|
return call(g:lsp_get_vim_completion_item[0], a:000)
|
|
endfunction
|
|
|
|
augroup lsp_completion_item_text_edit
|
|
autocmd!
|
|
autocmd CompleteDone * call <SID>apply_text_edits()
|
|
augroup END
|
|
|
|
function! s:apply_text_edits() abort
|
|
" textEdit support function(callin from CompleteDone).
|
|
"
|
|
" expected user_data structure:
|
|
" v:completed_item['user_data']: {
|
|
" 'vim-lsp/textEdit': {
|
|
" 'range': { ...(snip) },
|
|
" 'newText': 'yyy'
|
|
" },
|
|
" 'vim-lsp/additionalTextEdits': [
|
|
" {
|
|
" 'range': { ...(snip) },
|
|
" 'newText': 'yyy'
|
|
" },
|
|
" ...
|
|
" ],
|
|
" }
|
|
if !g:lsp_text_edit_enabled
|
|
doautocmd User lsp_complete_done
|
|
return
|
|
endif
|
|
|
|
" completion faild or not select complete item
|
|
if empty(v:completed_item)
|
|
doautocmd User lsp_complete_done
|
|
return
|
|
endif
|
|
|
|
" check user_data
|
|
if !has_key(v:completed_item, 'user_data')
|
|
doautocmd User lsp_complete_done
|
|
return
|
|
endif
|
|
|
|
" check user_data type is Dictionary and user_data['vim-lsp/textEdit']
|
|
try
|
|
let l:user_data = json_decode(v:completed_item['user_data'])
|
|
catch
|
|
" do nothing if user_data is not json type string.
|
|
doautocmd User lsp_complete_done
|
|
return
|
|
endtry
|
|
|
|
if type(l:user_data) != type({})
|
|
doautocmd User lsp_complete_done
|
|
return
|
|
endif
|
|
|
|
let l:all_text_edits = []
|
|
|
|
" if newText contains snippet markers, remove all them.
|
|
let l:snippet_marker_pos = -1
|
|
|
|
" expand textEdit range, for omni complet inserted text.
|
|
let l:text_edit = get(l:user_data, s:user_data_key, {})
|
|
if !empty(l:text_edit)
|
|
let l:expanded_text_edit = s:expand_range(l:text_edit, strchars(v:completed_item['word']))
|
|
" InsertTextFormat:Snippet
|
|
if get(l:user_data, s:user_data_insert_format_key, 0) == 2
|
|
let l:new_text = l:expanded_text_edit['newText']
|
|
let l:marker_pattern = '\<\$[0-9]\+\|\${[^}]\+}\>'
|
|
let l:snippet_marker_pos = matchstrpos(l:new_text, l:marker_pattern)[1] - 1
|
|
let l:expanded_text_edit['newText'] = substitute(l:new_text, l:marker_pattern, '', 'g')
|
|
endif
|
|
call add(l:all_text_edits, l:expanded_text_edit)
|
|
endif
|
|
|
|
if has_key(l:user_data, s:user_data_additional_edits_key)
|
|
let l:all_text_edits += l:user_data[s:user_data_additional_edits_key]
|
|
endif
|
|
|
|
" save cursor position in a mark, vim will move it appropriately when
|
|
" applying edits
|
|
let l:saved_mark = getpos("'a")
|
|
" move to end of newText but in two steps (as column may not exist yet)
|
|
let [l:pos, l:col_offset] = s:get_cursor_pos_and_edit_length(l:text_edit)
|
|
call setpos("'a", l:pos)
|
|
|
|
" apply textEdits
|
|
if !empty(l:all_text_edits)
|
|
call lsp#utils#text_edit#apply_text_edits(lsp#utils#get_buffer_uri(), l:all_text_edits)
|
|
" When user typed something character while popup menu is shwon, vim
|
|
" insert typed-character after CompleteDone occured. but the character
|
|
" should not be duplicated since the textEdit include the character.
|
|
" this remove the following character.
|
|
if l:snippet_marker_pos != -1
|
|
let l:oldpos = line('.')
|
|
let l:oldline = getline('.')
|
|
call timer_start(1, {_-> [
|
|
\ setline(l:oldpos, l:oldline),
|
|
\ execute('redraw', 1),
|
|
\ execute('doautocmd User lsp_complete_done', 1),
|
|
\] })
|
|
endif
|
|
return
|
|
endif
|
|
|
|
let l:pos = getpos("'a")
|
|
if l:snippet_marker_pos >= 0
|
|
let l:pos[2] += l:snippet_marker_pos
|
|
else
|
|
let l:pos[2] += l:col_offset
|
|
endif
|
|
call setpos("'a", l:saved_mark)
|
|
call setpos('.', l:pos)
|
|
|
|
doautocmd User lsp_complete_done
|
|
endfunction
|
|
|
|
function! s:expand_range(text_edit, expand_length) abort
|
|
let l:expanded_text_edit = a:text_edit
|
|
let l:expanded_text_edit['range']['end']['character'] += a:expand_length
|
|
|
|
return l:expanded_text_edit
|
|
endfunction
|
|
|
|
function! s:get_cursor_pos_and_edit_length(text_edit) abort
|
|
if !empty(a:text_edit)
|
|
let l:start = a:text_edit['range']['start']
|
|
let [l:line, l:col] = lsp#utils#position#_lsp_to_vim('%', l:start)
|
|
let l:length = len(a:text_edit['newText'])
|
|
let l:pos = [0, l:line, l:col, 0]
|
|
else
|
|
let l:length = 0
|
|
let l:pos = getpos('.')
|
|
endif
|
|
|
|
return [l:pos, l:length]
|
|
endfunction
|
|
|
|
function! lsp#omni#get_completion_item_kinds() abort
|
|
return map(keys(s:default_completion_item_kinds), {idx, key -> str2nr(key)})
|
|
endfunction
|
|
|
|
" }}}
|