I broke up with neovim....vim is my best friend now

This commit is contained in:
LinlyBoi
2023-04-30 08:14:07 +03:00
parent 0d185449c5
commit 4a4a6b1e81
5245 changed files with 468325 additions and 25 deletions

View File

@@ -0,0 +1,207 @@
" vint: -ProhibitUnusedVariable
function! lsp#ui#vim#code_action#complete(input, command, len) abort
let l:server_names = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
let l:kinds = []
for l:server_name in l:server_names
let l:kinds += lsp#capabilities#get_code_action_kinds(l:server_name)
endfor
return filter(copy(l:kinds), { _, kind -> kind =~ '^' . a:input })
endfunction
"
" @param option = {
" selection: v:true | v:false = Provide by CommandLine like `:'<,'>LspCodeAction`
" sync: v:true | v:false = Specify enable synchronous request. Example use case is `BufWritePre`
" query: string = Specify code action kind query. If query provided and then filtered code action is only one, invoke code action immediately.
" ui: 'float' | 'preview'
" }
"
function! lsp#ui#vim#code_action#do(option) abort
let l:selection = get(a:option, 'selection', v:false)
let l:sync = get(a:option, 'sync', v:false)
let l:query = get(a:option, 'query', '')
let l:ui = get(a:option, 'ui', g:lsp_code_action_ui)
if empty(l:ui)
let l:ui = lsp#utils#_has_popup_menu() ? 'float' : 'preview'
endif
let l:server_names = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
if len(l:server_names) == 0
return lsp#utils#error('Code action not supported for ' . &filetype)
endif
if l:selection
let l:range = lsp#utils#range#_get_recent_visual_range()
else
let l:range = lsp#utils#range#_get_current_line_range()
endif
let l:ctx = {
\ 'count': len(l:server_names),
\ 'results': [],
\}
let l:bufnr = bufnr('%')
let l:command_id = lsp#_new_command()
for l:server_name in l:server_names
let l:diagnostic = lsp#internal#diagnostics#under_cursor#get_diagnostic({'server': l:server_name})
call lsp#send_request(l:server_name, {
\ 'method': 'textDocument/codeAction',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'range': empty(l:diagnostic) || l:selection ? l:range : l:diagnostic['range'],
\ 'context': {
\ 'diagnostics' : empty(l:diagnostic) ? [] : [l:diagnostic],
\ 'only': ['', 'quickfix', 'refactor', 'refactor.extract', 'refactor.inline', 'refactor.rewrite', 'source', 'source.organizeImports'],
\ },
\ },
\ 'sync': l:sync,
\ 'on_notification': function('s:handle_code_action', [l:ui, l:ctx, l:server_name, l:command_id, l:sync, l:query, l:bufnr]),
\ })
endfor
echo 'Retrieving code actions ...'
endfunction
function! s:handle_code_action(ui, ctx, server_name, command_id, sync, query, bufnr, data) abort
" Ignore old request.
if a:command_id != lsp#_last_command()
return
endif
call add(a:ctx['results'], {
\ 'server_name': a:server_name,
\ 'data': a:data,
\})
let a:ctx['count'] -= 1
if a:ctx['count'] ># 0
return
endif
let l:total_code_actions = []
for l:result in a:ctx['results']
let l:server_name = l:result['server_name']
let l:data = l:result['data']
" Check response error.
if lsp#client#is_error(l:data['response'])
call lsp#utils#error('Failed to CodeAction for ' . l:server_name . ': ' . lsp#client#error_message(l:data['response']))
continue
endif
" Check code actions.
let l:code_actions = l:data['response']['result']
" Filter code actions.
if !empty(a:query)
let l:code_actions = filter(l:code_actions, { _, action -> get(action, 'kind', '') =~# '^' . a:query })
endif
if empty(l:code_actions)
continue
endif
for l:code_action in l:code_actions
let l:item = {
\ 'server_name': l:server_name,
\ 'code_action': l:code_action,
\ }
if get(l:code_action, 'isPreferred', v:false)
let l:total_code_actions = [l:item] + l:total_code_actions
else
call add(l:total_code_actions, l:item)
endif
endfor
endfor
if len(l:total_code_actions) == 0
echo 'No code actions found'
return
endif
call lsp#log('s:handle_code_action', l:total_code_actions)
if len(l:total_code_actions) == 1 && !empty(a:query)
let l:action = l:total_code_actions[0]
if s:handle_disabled_action(l:action) | return | endif
" Clear 'Retrieving code actions ...' message
echo ''
call s:handle_one_code_action(l:action['server_name'], a:sync, a:bufnr, l:action['code_action'])
return
endif
" Prompt to choose code actions when empty query provided.
let l:items = []
for l:action in l:total_code_actions
let l:title = printf('[%s] %s', l:action['server_name'], l:action['code_action']['title'])
if has_key(l:action['code_action'], 'kind') && l:action['code_action']['kind'] !=# ''
let l:title .= ' (' . l:action['code_action']['kind'] . ')'
endif
call add(l:items, { 'title': l:title, 'item': l:action })
endfor
if lsp#utils#_has_popup_menu() && a:ui ==? 'float'
call lsp#internal#ui#popupmenu#open({
\ 'title': 'Code actions',
\ 'items': mapnew(l:items, { idx, item -> item.title}),
\ 'pos': 'topleft',
\ 'line': 'cursor+1',
\ 'col': 'cursor',
\ 'callback': funcref('s:popup_accept_code_action', [a:sync, a:bufnr, l:items]),
\ })
else
call lsp#internal#ui#quickpick#open({
\ 'items': l:items,
\ 'key': 'title',
\ 'on_accept': funcref('s:quickpick_accept_code_action', [a:sync, a:bufnr]),
\ })
endif
endfunction
function! s:popup_accept_code_action(sync, bufnr, items, id, selected, ...) abort
if a:selected <= 0 | return | endif
let l:item = a:items[a:selected - 1]['item']
if s:handle_disabled_action(l:item) | return | endif
call s:handle_one_code_action(l:item['server_name'], a:sync, a:bufnr, l:item['code_action'])
execute('doautocmd <nomodeline> User lsp_float_closed')
endfunction
function! s:quickpick_accept_code_action(sync, bufnr, data, ...) abort
call lsp#internal#ui#quickpick#close()
if empty(a:data['items']) | return | endif
let l:selected = a:data['items'][0]['item']
if s:handle_disabled_action(l:selected) | return | endif
call s:handle_one_code_action(l:selected['server_name'], a:sync, a:bufnr, l:selected['code_action'])
endfunction
function! s:handle_disabled_action(code_action) abort
if has_key(a:code_action, 'disabled')
echo 'This action is disabled: ' . a:code_action['disabled']['reason']
return v:true
endif
return v:false
endfunction
function! s:handle_one_code_action(server_name, sync, bufnr, command_or_code_action) abort
" has WorkspaceEdit.
if has_key(a:command_or_code_action, 'edit')
call lsp#utils#workspace_edit#apply_workspace_edit(a:command_or_code_action['edit'])
endif
" Command.
if has_key(a:command_or_code_action, 'command') && type(a:command_or_code_action['command']) == type('')
call lsp#ui#vim#execute_command#_execute({
\ 'server_name': a:server_name,
\ 'command_name': get(a:command_or_code_action, 'command', ''),
\ 'command_args': get(a:command_or_code_action, 'arguments', v:null),
\ 'sync': a:sync,
\ 'bufnr': a:bufnr,
\ })
" has Command.
elseif has_key(a:command_or_code_action, 'command') && type(a:command_or_code_action['command']) == type({})
call lsp#ui#vim#execute_command#_execute({
\ 'server_name': a:server_name,
\ 'command_name': get(a:command_or_code_action['command'], 'command', ''),
\ 'command_args': get(a:command_or_code_action['command'], 'arguments', v:null),
\ 'sync': a:sync,
\ 'bufnr': a:bufnr,
\ })
endif
endfunction

View File

@@ -0,0 +1,137 @@
" https://microsoft.github.io/language-server-protocol/specification#textDocument_codeLens
" @param option = {
" }
"
function! lsp#ui#vim#code_lens#do(option) abort
let l:sync = get(a:option, 'sync', v:false)
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_code_lens_provider(v:val)')
if len(l:servers) == 0
return lsp#utils#error('Code lens not supported for ' . &filetype)
endif
redraw | echo 'Retrieving codelens ...'
let l:bufnr = bufnr('%')
call lsp#callbag#pipe(
\ lsp#callbag#fromList(l:servers),
\ lsp#callbag#flatMap({server->
\ lsp#callbag#pipe(
\ lsp#request(server, {
\ 'method': 'textDocument/codeLens',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(l:bufnr),
\ },
\ }),
\ lsp#callbag#map({x->x['response']['result']}),
\ lsp#callbag#filter({codelenses->!empty(codelenses)}),
\ lsp#callbag#flatMap({codelenses->
\ lsp#callbag#pipe(
\ lsp#callbag#fromList(codelenses),
\ lsp#callbag#flatMap({codelens->
\ has_key(codelens, 'command') ? lsp#callbag#of(codelens) : s:resolve_codelens(server, codelens)}),
\ )
\ }),
\ lsp#callbag#map({codelens->{ 'server': server, 'codelens': codelens }}),
\ )
\ }),
\ lsp#callbag#reduce({acc,curr->add(acc, curr)}, []),
\ lsp#callbag#flatMap({x->s:chooseCodeLens(x, l:bufnr)}),
\ lsp#callbag#tap({x-> lsp#ui#vim#execute_command#_execute({
\ 'server_name': x['server'],
\ 'command_name': get(x['codelens']['command'], 'command', ''),
\ 'command_args': get(x['codelens']['command'], 'arguments', v:null),
\ 'bufnr': l:bufnr,
\ })}),
\ lsp#callbag#takeUntil(lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'command')}),
\ )),
\ lsp#callbag#subscribe({
\ 'error': {e->lsp#utils#error('Error running codelens ' . json_encode(e))},
\ }),
\ )
endfunction
function! s:resolve_codelens(server, codelens) abort
" TODO: return callbag#lsp#empty() if codelens resolve not supported by server
return lsp#callbag#pipe(
\ lsp#request(a:server, {
\ 'method': 'codeLens/resolve',
\ 'params': a:codelens
\ }),
\ lsp#callbag#map({x->x['response']['result']}),
\ )
endfunction
function! s:chooseCodeLens(items, bufnr) abort
redraw | echo 'Select codelens:'
if empty(a:items)
return lsp#callbag#throwError('No codelens found')
endif
return lsp#callbag#create(function('s:quickpick_open', [a:items, a:bufnr]))
endfunction
function! lsp#ui#vim#code_lens#_get_subtitle(item) abort
" Since element of arguments property of Command interface is defined as any in LSP spec, it is
" up to the language server implementation.
" Currently this only impacts rust-analyzer. See #1118 for more details.
if !has_key(a:item['codelens']['command'], 'arguments')
return ''
endif
let l:arguments = a:item['codelens']['command']['arguments']
for l:argument in l:arguments
if type(l:argument) != type({}) || !has_key(l:argument, 'label')
return ''
endif
endfor
return ': ' . join(map(copy(l:arguments), 'v:val["label"]'), ' > ')
endfunction
function! s:quickpick_open(items, bufnr, next, error, complete) abort
if empty(a:items)
return lsp#callbag#empty()
endif
let l:items = []
for l:item in a:items
let l:title = printf("[%s] %s%s\t| L%s:%s",
\ l:item['server'],
\ l:item['codelens']['command']['title'],
\ lsp#ui#vim#code_lens#_get_subtitle(l:item),
\ lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['codelens']['range']['start']),
\ getbufline(a:bufnr, lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['codelens']['range']['start']))[0])
call add(l:items, { 'title': l:title, 'item': l:item })
endfor
call lsp#internal#ui#quickpick#open({
\ 'items': l:items,
\ 'key': 'title',
\ 'on_accept': function('s:quickpick_accept', [a:next, a:error, a:complete]),
\ 'on_cancel': function('s:quickpick_cancel', [a:next, a:error, a:complete]),
\ })
return function('s:quickpick_dispose')
endfunction
function! s:quickpick_dispose() abort
call lsp#internal#ui#quickpick#close()
endfunction
function! s:quickpick_accept(next, error, complete, data, ...) abort
call lsp#internal#ui#quickpick#close()
let l:items = a:data['items']
if len(l:items) > 0
call a:next(l:items[0]['item'])
endif
call a:complete()
endfunction
function! s:quickpick_cancel(next, error, complete, ...) abort
call a:complete()
endfunction

View File

@@ -0,0 +1,303 @@
" vint: -ProhibitUnusedVariable
"
let s:context = {}
function! lsp#ui#vim#completion#_setup() abort
augroup lsp_ui_vim_completion
autocmd!
autocmd CompleteDone * call s:on_complete_done()
augroup END
endfunction
function! lsp#ui#vim#completion#_disable() abort
augroup lsp_ui_vim_completion
autocmd!
augroup END
endfunction
"
" After CompleteDone, v:complete_item's word has been inserted into the line.
" Yet not inserted commit characters.
"
" below example uses | as cursor position.
"
" 1. `call getbuf|`<C-x><C-o>
" 2. select `getbufline` item.
" 3. Insert commit characters. e.g. `(`
" 4. fire CompleteDone, then the line is `call getbufline|`
" 5. call feedkeys to call `s:on_complete_done_after`
" 6. then the line is `call getbufline(|` in `s:on_complete_done_after`
"
function! s:on_complete_done() abort
" Sometimes, vim occurs `CompleteDone` unexpectedly.
" We try to detect it by checking empty completed_item.
if empty(v:completed_item) || get(v:completed_item, 'word', '') ==# '' && get(v:completed_item, 'abbr', '') ==# ''
doautocmd <nomodeline> User lsp_complete_done
return
endif
" Try to get managed user_data.
let l:managed_user_data = lsp#omni#get_managed_user_data_from_completed_item(v:completed_item)
" Clear managed user_data.
call lsp#omni#_clear_managed_user_data_map()
" If managed user_data does not exists, skip it.
if empty(l:managed_user_data)
doautocmd <nomodeline> User lsp_complete_done
return
endif
let s:context['done_line'] = getline('.')
let s:context['done_line_nr'] = line('.')
let s:context['completed_item'] = copy(v:completed_item)
let s:context['done_position'] = lsp#utils#position#vim_to_lsp('%', getpos('.')[1 : 2])
let s:context['complete_position'] = l:managed_user_data['complete_position']
let s:context['server_name'] = l:managed_user_data['server_name']
let s:context['completion_item'] = l:managed_user_data['completion_item']
let s:context['start_character'] = l:managed_user_data['start_character']
call feedkeys(printf("\<C-r>=<SNR>%d_on_complete_done_after()\<CR>", s:SID()), 'n')
endfunction
"
" Apply textEdit or insertText(snippet) and additionalTextEdits.
"
function! s:on_complete_done_after() abort
" Clear message line. feedkeys above leave garbage on message line.
echo ''
" Ignore process if the mode() is not insert-mode after feedkeys.
if mode(1)[0] !=# 'i'
return ''
endif
let l:done_line = s:context['done_line']
let l:done_line_nr = s:context['done_line_nr']
let l:completed_item = s:context['completed_item']
let l:done_position = s:context['done_position']
let l:complete_position = s:context['complete_position']
let l:server_name = s:context['server_name']
let l:completion_item = s:context['completion_item']
let l:start_character = s:context['start_character']
" check the commit characters are <BS> or <C-w>.
if line('.') ==# l:done_line_nr && strlen(getline('.')) < strlen(l:done_line)
doautocmd <nomodeline> User lsp_complete_done
return ''
endif
" Do nothing if text_edit is disabled.
if !g:lsp_text_edit_enabled
doautocmd <nomodeline> User lsp_complete_done
return ''
endif
let l:completion_item = s:resolve_completion_item(l:completion_item, l:server_name)
" clear completed string if need.
let l:is_expandable = s:is_expandable(l:done_line, l:done_position, l:complete_position, l:completion_item, l:completed_item)
if l:is_expandable
call s:clear_auto_inserted_text(l:done_line, l:done_position, l:complete_position)
endif
" apply additionalTextEdits.
if has_key(l:completion_item, 'additionalTextEdits') && !empty(l:completion_item['additionalTextEdits'])
call lsp#utils#text_edit#apply_text_edits(lsp#utils#get_buffer_uri(bufnr('%')), l:completion_item['additionalTextEdits'])
endif
" snippet or textEdit.
if l:is_expandable
" At this timing, the cursor may have been moved by additionalTextEdit, so we use overflow information instead of textEdit itself.
if type(get(l:completion_item, 'textEdit', v:null)) == type({})
let l:range = lsp#utils#text_edit#get_range(l:completion_item['textEdit'])
let l:overflow_before = max([0, l:start_character - l:range['start']['character']])
let l:overflow_after = max([0, l:range['end']['character'] - l:complete_position['character']])
let l:text = l:completion_item['textEdit']['newText']
else
let l:overflow_before = 0
let l:overflow_after = 0
let l:text = s:get_completion_text(l:completion_item)
endif
" apply snipept or text_edit
let l:position = lsp#utils#position#vim_to_lsp('%', getpos('.')[1 : 2])
let l:range = {
\ 'start': {
\ 'line': l:position['line'],
\ 'character': l:position['character'] - (l:complete_position['character'] - l:start_character) - l:overflow_before,
\ },
\ 'end': {
\ 'line': l:position['line'],
\ 'character': l:position['character'] + l:overflow_after,
\ }
\ }
if get(l:completion_item, 'insertTextFormat', 1) == 2
" insert Snippet.
call lsp#utils#text_edit#apply_text_edits('%', [{ 'range': l:range, 'newText': '' }])
call cursor(lsp#utils#position#lsp_to_vim('%', l:range['start']))
if exists('g:lsp_snippet_expand') && len(g:lsp_snippet_expand) > 0
call g:lsp_snippet_expand[0]({ 'snippet': l:text })
else
call s:simple_expand_text(l:text)
endif
else
" apply TextEdit.
call lsp#utils#text_edit#apply_text_edits('%', [{ 'range': l:range, 'newText': l:text }])
" The VSCode always apply completion word as snippet.
" It means we should place cursor to end of new inserted text as snippet does.
let l:lines = lsp#utils#_split_by_eol(l:text)
let l:start = l:range.start
let l:start.line += len(l:lines) - 1
let l:start.character += strchars(l:lines[-1])
call cursor(lsp#utils#position#lsp_to_vim('%', l:start))
endif
endif
doautocmd <nomodeline> User lsp_complete_done
return ''
endfunction
"
" is_expandable
"
function! s:is_expandable(done_line, done_position, complete_position, completion_item, completed_item) abort
if get(a:completion_item, 'textEdit', v:null) isnot# v:null
let l:range = lsp#utils#text_edit#get_range(a:completion_item['textEdit'])
if l:range['start']['line'] != l:range['end']['line']
return v:true
endif
" compute if textEdit will change text.
let l:completed_before = strcharpart(a:done_line, 0, a:complete_position['character'])
let l:completed_after = strcharpart(a:done_line, a:done_position['character'], strchars(a:done_line) - a:done_position['character'])
let l:completed_line = l:completed_before . l:completed_after
let l:text_edit_before = strcharpart(l:completed_line, 0, l:range['start']['character'])
let l:text_edit_after = strcharpart(l:completed_line, l:range['end']['character'], strchars(l:completed_line) - l:range['end']['character'])
return a:done_line !=# l:text_edit_before . s:trim_unmeaning_tabstop(a:completion_item['textEdit']['newText']) . l:text_edit_after
endif
return s:get_completion_text(a:completion_item) !=# s:trim_unmeaning_tabstop(a:completed_item['word'])
endfunction
"
" trim_unmeaning_tabstop
"
function! s:trim_unmeaning_tabstop(text) abort
return substitute(a:text, '\%(\$0\|\${0}\)$', '', 'g')
endfunction
"
" Try `completionItem/resolve` if it possible.
"
function! s:resolve_completion_item(completion_item, server_name) abort
" server_name is not provided.
if empty(a:server_name)
return a:completion_item
endif
" check server capabilities.
let l:capabilities = lsp#get_server_capabilities(a:server_name)
if !has_key(l:capabilities, 'completionProvider')
\ || type(l:capabilities['completionProvider']) != v:t_dict
\ || !has_key(l:capabilities['completionProvider'], 'resolveProvider')
\ || !l:capabilities['completionProvider']['resolveProvider']
return a:completion_item
endif
let l:ctx = {}
let l:ctx['response'] = {}
function! l:ctx['callback'](data) abort
let l:self['response'] = a:data['response']
endfunction
try
call lsp#send_request(a:server_name, {
\ 'method': 'completionItem/resolve',
\ 'params': a:completion_item,
\ 'sync': 1,
\ 'sync_timeout': g:lsp_completion_resolve_timeout,
\ 'on_notification': function(l:ctx['callback'], [], l:ctx)
\ })
catch /.*/
call lsp#log('s:resolve_completion_item', 'request timeout.')
endtry
if empty(l:ctx['response'])
return a:completion_item
endif
if lsp#client#is_error(l:ctx['response'])
return a:completion_item
endif
if empty(l:ctx['response']['result'])
return a:completion_item
endif
return l:ctx['response']['result']
endfunction
"
" Remove additional inserted text
"
" LSP server knows only `complete_position` so we should remove inserted text until complete_position.
"
function! s:clear_auto_inserted_text(done_line, done_position, complete_position) abort
let l:before = strcharpart(a:done_line, 0, a:complete_position['character'])
let l:after = strcharpart(a:done_line, a:done_position['character'], (strchars(a:done_line) - a:done_position['character']))
call setline('.', l:before . l:after)
call cursor([a:done_position['line'] + 1, strlen(l:before) + 1])
endfunction
"
" Expand text
"
function! s:simple_expand_text(text) abort
let l:pos = {
\ 'line': line('.') - 1,
\ 'character': lsp#utils#to_char('%', line('.'), col('.'))
\ }
" Remove placeholders and get first placeholder position that use to cursor position.
" e.g. `|getbufline(${1:expr}, ${2:lnum})${0}` to getbufline(|,)
let l:text = substitute(a:text, '\$\%({[0-9]\+\%(:\(\\.\|[^}]\+\)*\)}\|[0-9]\+\)', '\=substitute(submatch(1), "\\", "", "g")', 'g')
let l:offset = match(a:text, '\$\%({[0-9]\+\%(:\(\\.\|[^}]\+\)*\)}\|[0-9]\+\)')
if l:offset == -1
let l:offset = strchars(l:text)
endif
call lsp#utils#text_edit#apply_text_edits(lsp#utils#get_buffer_uri(bufnr('%')), [{
\ 'range': {
\ 'start': l:pos,
\ 'end': l:pos
\ },
\ 'newText': l:text
\ }])
let l:pos = lsp#utils#position#lsp_to_vim('%', {
\ 'line': l:pos['line'],
\ 'character': l:pos['character'] + l:offset
\ })
call cursor(l:pos)
endfunction
"
" Get completion text from CompletionItem. Fallback to label when insertText
" is falsy
"
function! s:get_completion_text(completion_item) abort
let l:text = get(a:completion_item, 'insertText', '')
if empty(l:text)
let l:text = a:completion_item['label']
endif
return l:text
endfunction
"
" Get script id that uses to call `s:` function in feedkeys.
"
function! s:SID() abort
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_SID$')
endfunction

View File

@@ -0,0 +1,64 @@
let s:commands = {}
"
" @param {name} = string
" @param {callback} = funcref
"
function! lsp#ui#vim#execute_command#_register(command_name, callback) abort
if has_key(s:commands, a:command_name)
throw printf('lsp#ui#vim#execute_command#_register_command: %s already registered.', a:command_name)
endif
let s:commands[a:command_name] = a:callback
endfunction
"
" TODO: This method does not handle any return value.
"
function! lsp#ui#vim#execute_command#_execute(params) abort
let l:command_name = a:params['command_name']
let l:command_args = get(a:params, 'command_args', v:null)
let l:server_name = get(a:params, 'server_name', '')
let l:bufnr = get(a:params, 'bufnr', -1)
let l:sync = get(a:params, 'sync', v:false)
" create command.
let l:command = { 'command': l:command_name }
if l:command_args isnot v:null
let l:command['arguments'] = l:command_args
endif
" execute command on local.
if has_key(s:commands, l:command_name)
try
call s:commands[l:command_name]({
\ 'bufnr': l:bufnr,
\ 'server_name': l:server_name,
\ 'command': l:command,
\ })
catch /.*/
call lsp#utils#error(printf('Execute command failed: %s', string(a:params)))
endtry
return
endif
" execute command on server.
if !empty(l:server_name)
call lsp#send_request(l:server_name, {
\ 'method': 'workspace/executeCommand',
\ 'params': l:command,
\ 'sync': l:sync,
\ 'on_notification': function('s:handle_execute_command', [l:server_name, l:command]),
\ })
endif
endfunction
"
" handle workspace/executeCommand response
"
function! s:handle_execute_command(server_name, command, data) abort
if lsp#client#is_error(a:data['response'])
call lsp#utils#error('Execute command failed on ' . a:server_name . ': ' . string(a:command) . ' -> ' . string(a:data))
endif
endfunction

View File

@@ -0,0 +1,201 @@
let s:folding_ranges = {}
let s:textprop_name = 'vim-lsp-folding-linenr'
function! s:find_servers() abort
return filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_folding_range_provider(v:val)')
endfunction
function! lsp#ui#vim#folding#fold(sync) abort
let l:servers = s:find_servers()
if len(l:servers) == 0
call lsp#utils#error('Folding not supported for ' . &filetype)
return
endif
let l:server = l:servers[0]
call lsp#ui#vim#folding#send_request(l:server, bufnr('%'), a:sync)
endfunction
function! s:set_textprops(buf) abort
" Use zero-width text properties to act as a sort of "mark" in the buffer.
" This is used to remember the line numbers at the time the request was
" sent. We will let Vim handle updating the line numbers when the user
" inserts or deletes text.
" Skip if the buffer doesn't exist. This might happen when a buffer is
" opened and quickly deleted.
if !bufloaded(a:buf) | return | endif
" Create text property, if not already defined
silent! call prop_type_add(s:textprop_name, {'bufnr': a:buf, 'priority': lsp#internal#textprop#priority('folding')})
let l:line_count = s:get_line_count_buf(a:buf)
" First, clear all markers from the previous run
call prop_remove({'type': s:textprop_name, 'bufnr': a:buf}, 1, l:line_count)
" Add markers to each line
let l:i = 1
while l:i <= l:line_count
call prop_add(l:i, 1, {'bufnr': a:buf, 'type': s:textprop_name, 'id': l:i})
let l:i += 1
endwhile
endfunction
function! s:get_line_count_buf(buf) abort
if !has('patch-8.1.1967')
return line('$')
endif
let l:winids = win_findbuf(a:buf)
return empty(l:winids) ? line('$') : line('$', l:winids[0])
endfunction
function! lsp#ui#vim#folding#send_request(server_name, buf, sync) abort
if !lsp#capabilities#has_folding_range_provider(a:server_name)
return
endif
if !g:lsp_fold_enabled
call lsp#log('Skip sending fold request: folding was disabled explicitly')
return
endif
if has('textprop')
call s:set_textprops(a:buf)
endif
call lsp#send_request(a:server_name, {
\ 'method': 'textDocument/foldingRange',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(a:buf)
\ },
\ 'on_notification': function('s:handle_fold_request', [a:server_name]),
\ 'sync': a:sync,
\ 'bufnr': a:buf
\ })
endfunction
function! s:foldexpr(server, buf, linenr) abort
let l:foldlevel = 0
let l:prefix = ''
for l:folding_range in s:folding_ranges[a:server][a:buf]
if type(l:folding_range) == type({}) &&
\ has_key(l:folding_range, 'startLine') &&
\ has_key(l:folding_range, 'endLine')
let l:start = l:folding_range['startLine'] + 1
let l:end = l:folding_range['endLine'] + 1
if (l:start <= a:linenr) && (a:linenr <= l:end)
let l:foldlevel += 1
endif
if l:start == a:linenr
let l:prefix = '>'
elseif l:end == a:linenr
let l:prefix = '<'
endif
endif
endfor
" Only return marker if a fold starts/ends at this line.
" Otherwise, return '='.
return (l:prefix ==# '') ? '=' : (l:prefix . l:foldlevel)
endfunction
" Searches for text property of the correct type on the given line.
" Returns the original linenr on success, or -1 if no textprop of the correct
" type is associated with this line.
function! s:get_textprop_line(linenr) abort
let l:props = filter(prop_list(a:linenr), {idx, prop -> prop['type'] ==# s:textprop_name})
if empty(l:props)
return -1
else
return l:props[0]['id']
endif
endfunction
function! lsp#ui#vim#folding#foldexpr() abort
let l:servers = s:find_servers()
if len(l:servers) == 0
return
endif
let l:server = l:servers[0]
if has('textprop')
" Does the current line have a textprop with original line info?
let l:textprop_line = s:get_textprop_line(v:lnum)
if l:textprop_line == -1
" No information for current line available, so use indent for
" previous line.
return '='
else
" Info available, use foldexpr as it would be with original line
" number
return s:foldexpr(l:server, bufnr('%'), l:textprop_line)
endif
else
return s:foldexpr(l:server, bufnr('%'), v:lnum)
endif
endfunction
function! lsp#ui#vim#folding#foldtext() abort
let l:num_lines = v:foldend - v:foldstart + 1
let l:summary = getline(v:foldstart) . '...'
" Join all lines in the fold
let l:combined_lines = ''
let l:i = v:foldstart
while l:i <= v:foldend
let l:combined_lines .= getline(l:i) . ' '
let l:i += 1
endwhile
" Check if we're in a comment
let l:comment_regex = '\V' . substitute(&l:commentstring, '%s', '\\.\\*', '')
if l:combined_lines =~? l:comment_regex
let l:summary = l:combined_lines
endif
return l:summary . ' (' . l:num_lines . ' ' . (l:num_lines == 1 ? 'line' : 'lines') . ') '
endfunction
function! s:handle_fold_request(server, data) abort
if lsp#client#is_error(a:data) || !has_key(a:data, 'response') || !has_key(a:data['response'], 'result')
return
endif
let l:result = a:data['response']['result']
if type(l:result) != type([])
return
endif
let l:uri = a:data['request']['params']['textDocument']['uri']
let l:path = lsp#utils#uri_to_path(l:uri)
let l:bufnr = bufnr(l:path)
if l:bufnr < 0
return
endif
if !has_key(s:folding_ranges, a:server)
let s:folding_ranges[a:server] = {}
endif
let s:folding_ranges[a:server][l:bufnr] = l:result
" Set 'foldmethod' back to 'expr', which forces a re-evaluation of
" 'foldexpr'. Only do this if the user hasn't changed 'foldmethod',
" and this is the correct buffer.
for l:winid in win_findbuf(l:bufnr)
if getwinvar(l:winid, '&foldmethod') ==# 'expr'
call setwinvar(l:winid, '&foldmethod', 'expr')
endif
endfor
endfunction

View File

@@ -0,0 +1,450 @@
let s:use_vim_popup = has('patch-8.1.1517') && g:lsp_preview_float && !has('nvim')
let s:use_nvim_float = exists('*nvim_open_win') && g:lsp_preview_float && has('nvim')
let s:use_preview = !s:use_vim_popup && !s:use_nvim_float
function! s:import_modules() abort
if exists('s:Markdown') | return | endif
let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown')
let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent')
let s:Window = vital#lsp#import('VS.Vim.Window')
let s:Text = vital#lsp#import('VS.LSP.Text')
endfunction
let s:winid = v:false
let s:prevwin = v:false
let s:preview_data = v:false
function! s:vim_popup_closed(...) abort
let s:preview_data = v:false
endfunction
function! lsp#ui#vim#output#closepreview() abort
if win_getid() ==# s:winid
" Don't close if window got focus
return
endif
if s:winid == v:false
return
endif
"closing floats in vim8.1 must use popup_close()
"nvim must use nvim_win_close. pclose is not reliable and does not always work
if s:use_vim_popup && s:winid
call popup_close(s:winid)
elseif s:use_nvim_float && s:winid
silent! call nvim_win_close(s:winid, 0)
else
pclose
endif
let s:winid = v:false
let s:preview_data = v:false
augroup lsp_float_preview_close
augroup end
autocmd! lsp_float_preview_close CursorMoved,CursorMovedI,VimResized *
doautocmd <nomodeline> User lsp_float_closed
endfunction
function! lsp#ui#vim#output#focuspreview() abort
if s:is_cmdwin()
return
endif
" This does not work for vim8.1 popup but will work for nvim and old preview
if s:winid
if win_getid() !=# s:winid
let s:prevwin = win_getid()
call win_gotoid(s:winid)
elseif s:prevwin
" Temporarily disable hooks
" TODO: remove this when closing logic is able to distinguish different move directions
autocmd! lsp_float_preview_close CursorMoved,CursorMovedI,VimResized *
call win_gotoid(s:prevwin)
call s:add_float_closing_hooks()
let s:prevwin = v:false
endif
endif
endfunction
function! s:bufwidth() abort
let l:width = winwidth(0)
let l:numberwidth = max([&numberwidth, strlen(line('$'))+1])
let l:numwidth = (&number || &relativenumber)? l:numberwidth : 0
let l:foldwidth = &foldcolumn
if &signcolumn ==? 'yes'
let l:signwidth = 2
elseif &signcolumn ==? 'auto'
let l:signs = execute(printf('sign place buffer=%d', bufnr('')))
let l:signs = split(l:signs, "\n")
let l:signwidth = len(l:signs)>2? 2: 0
else
let l:signwidth = 0
endif
return l:width - l:numwidth - l:foldwidth - l:signwidth
endfunction
function! s:get_float_positioning(height, width) abort
let l:height = a:height
let l:width = a:width
" TODO: add option to configure it 'docked' at the bottom/top/right
" NOTE: screencol() and screenrow() start from (1,1)
" but the popup window co-ordinates start from (0,0)
" Very convenient!
" For a simple single-line 'tooltip', the following
" two lines are enough to determine the position
let l:col = screencol()
let l:row = screenrow()
let l:height = min([l:height, max([&lines - &cmdheight - l:row, &previewheight])])
let l:style = 'minimal'
" Positioning is not window but screen relative
let l:opts = {
\ 'relative': 'editor',
\ 'row': l:row,
\ 'col': l:col,
\ 'width': l:width,
\ 'height': l:height,
\ 'style': l:style,
\ }
return l:opts
endfunction
function! lsp#ui#vim#output#floatingpreview(data) abort
if s:use_nvim_float
let l:buf = nvim_create_buf(v:false, v:true)
" Try to get as much space around the cursor, but at least 10x10
let l:width = max([s:bufwidth(), 10])
let l:height = max([&lines - winline() + 1, winline() - 1, 10])
if g:lsp_preview_max_height > 0
let l:height = min([g:lsp_preview_max_height, l:height])
endif
let l:opts = s:get_float_positioning(l:height, l:width)
let s:winid = nvim_open_win(l:buf, v:false, l:opts)
call nvim_win_set_option(s:winid, 'winhl', 'Normal:Pmenu,NormalNC:Pmenu')
call nvim_win_set_option(s:winid, 'foldenable', v:false)
call nvim_win_set_option(s:winid, 'wrap', v:true)
call nvim_win_set_option(s:winid, 'statusline', '')
call nvim_win_set_option(s:winid, 'number', v:false)
call nvim_win_set_option(s:winid, 'relativenumber', v:false)
call nvim_win_set_option(s:winid, 'cursorline', v:false)
call nvim_win_set_option(s:winid, 'cursorcolumn', v:false)
call nvim_win_set_option(s:winid, 'colorcolumn', '')
call nvim_win_set_option(s:winid, 'signcolumn', 'no')
" Enable closing the preview with esc, but map only in the scratch buffer
call nvim_buf_set_keymap(l:buf, 'n', '<esc>', ':pclose<cr>', {'silent': v:true})
elseif s:use_vim_popup
let l:options = {
\ 'moved': 'any',
\ 'border': [1, 1, 1, 1],
\ 'callback': function('s:vim_popup_closed')
\ }
if g:lsp_preview_max_width > 0
let l:options['maxwidth'] = g:lsp_preview_max_width
endif
if g:lsp_preview_max_height > 0
let l:options['maxheight'] = g:lsp_preview_max_height
endif
let s:winid = popup_atcursor('...', l:options)
endif
return s:winid
endfunction
function! lsp#ui#vim#output#setcontent(winid, lines, ft) abort
if s:use_vim_popup
" vim popup
call setbufline(winbufnr(a:winid), 1, a:lines)
call setbufvar(winbufnr(a:winid), '&filetype', a:ft . '.lsp-hover')
elseif s:use_nvim_float
" nvim floating
call nvim_buf_set_lines(winbufnr(a:winid), 0, -1, v:false, a:lines)
call nvim_buf_set_option(winbufnr(a:winid), 'readonly', v:true)
call nvim_buf_set_option(winbufnr(a:winid), 'modifiable', v:false)
call nvim_buf_set_option(winbufnr(a:winid), 'filetype', a:ft.'.lsp-hover')
call nvim_win_set_cursor(a:winid, [1, 0])
elseif s:use_preview
" preview window
call setbufline(winbufnr(a:winid), 1, a:lines)
call setbufvar(winbufnr(a:winid), '&filetype', a:ft . '.lsp-hover')
endif
endfunction
function! lsp#ui#vim#output#adjust_float_placement(bufferlines, maxwidth) abort
if s:use_nvim_float
let l:win_config = {}
let l:height = min([winheight(s:winid), a:bufferlines])
let l:width = min([winwidth(s:winid), a:maxwidth])
let l:win_config = s:get_float_positioning(l:height, l:width)
call nvim_win_set_config(s:winid, l:win_config )
endif
endfunction
function! s:add_float_closing_hooks() abort
if g:lsp_preview_autoclose
augroup lsp_float_preview_close
autocmd! lsp_float_preview_close CursorMoved,CursorMovedI,VimResized *
autocmd CursorMoved,CursorMovedI,VimResized * call lsp#ui#vim#output#closepreview()
augroup END
endif
endfunction
function! lsp#ui#vim#output#getpreviewwinid() abort
return s:winid
endfunction
function! s:open_preview(data) abort
if s:use_vim_popup || s:use_nvim_float
let l:winid = lsp#ui#vim#output#floatingpreview(a:data)
else
execute &previewheight.'new'
let l:winid = win_getid()
endif
return l:winid
endfunction
function! s:set_cursor(current_window_id, options) abort
if !has_key(a:options, 'cursor')
return
endif
if s:use_nvim_float
" Neovim floats
" Go back to the preview window to set the cursor
call win_gotoid(s:winid)
let l:old_scrolloff = &scrolloff
let &scrolloff = 0
call nvim_win_set_cursor(s:winid, [a:options['cursor']['line'], a:options['cursor']['col']])
call s:align_preview(a:options)
" Finally, go back to the original window
call win_gotoid(a:current_window_id)
let &scrolloff = l:old_scrolloff
elseif s:use_vim_popup
" Vim popups
function! AlignVimPopup(timer) closure abort
call s:align_preview(a:options)
endfunction
call timer_start(0, function('AlignVimPopup'))
else
" Preview
" Don't use 'scrolloff', it might mess up the cursor's position
let &l:scrolloff = 0
call cursor(a:options['cursor']['line'], a:options['cursor']['col'])
call s:align_preview(a:options)
endif
endfunction
function! s:align_preview(options) abort
if !has_key(a:options, 'cursor') ||
\ !has_key(a:options['cursor'], 'align')
return
endif
let l:align = a:options['cursor']['align']
if s:use_vim_popup
" Vim popups
let l:pos = popup_getpos(s:winid)
let l:below = winline() < winheight(0) / 2
if l:below
let l:height = min([l:pos['core_height'], winheight(0) - winline() - 2])
else
let l:height = min([l:pos['core_height'], winline() - 3])
endif
let l:width = l:pos['core_width']
let l:options = {
\ 'minwidth': l:width,
\ 'maxwidth': l:width,
\ 'minheight': l:height,
\ 'maxheight': l:height,
\ 'pos': l:below ? 'topleft' : 'botleft',
\ 'line': l:below ? 'cursor+1' : 'cursor-1'
\ }
if l:align ==? 'top'
let l:options['firstline'] = a:options['cursor']['line']
elseif l:align ==? 'center'
let l:options['firstline'] = a:options['cursor']['line'] - (l:height - 1) / 2
elseif l:align ==? 'bottom'
let l:options['firstline'] = a:options['cursor']['line'] - l:height + 1
endif
call popup_setoptions(s:winid, l:options)
redraw!
else
" Preview and Neovim floats
if l:align ==? 'top'
normal! zt
elseif l:align ==? 'center'
normal! zz
elseif l:align ==? 'bottom'
normal! zb
endif
endif
endfunction
function! lsp#ui#vim#output#get_size_info(winid) abort
" Get size information while still having the buffer active
let l:buffer = winbufnr(a:winid)
let l:maxwidth = max(map(getbufline(l:buffer, 1, '$'), 'strdisplaywidth(v:val)'))
let l:bufferlines = 0
if g:lsp_preview_max_width > 0
let l:maxwidth = min([g:lsp_preview_max_width, l:maxwidth])
" Determine, for each line, how many "virtual" lines it spans, and add
" these together for all lines in the buffer
for l:line in getbufline(l:buffer, 1, '$')
let l:num_lines = str2nr(string(ceil(strdisplaywidth(l:line) * 1.0 / g:lsp_preview_max_width)))
let l:bufferlines += max([l:num_lines, 1])
endfor
else
if s:use_vim_popup
let l:bufferlines = line('$', a:winid)
elseif s:use_nvim_float
let l:bufferlines = nvim_buf_line_count(winbufnr(a:winid))
endif
endif
return [l:bufferlines, l:maxwidth]
endfunction
function! lsp#ui#vim#output#float_supported() abort
return s:use_vim_popup || s:use_nvim_float
endfunction
function! lsp#ui#vim#output#preview(server, data, options) abort
if s:is_cmdwin()
return
endif
if s:winid && type(s:preview_data) ==# type(a:data)
\ && s:preview_data ==# a:data
\ && type(g:lsp_preview_doubletap) ==# 3
\ && len(g:lsp_preview_doubletap) >= 1
\ && type(g:lsp_preview_doubletap[0]) ==# 2
\ && index(['i', 's'], mode()[0]) ==# -1
echo ''
return call(g:lsp_preview_doubletap[0], [])
endif
" Close any previously opened preview window
call lsp#ui#vim#output#closepreview()
let l:current_window_id = win_getid()
let s:winid = s:open_preview(a:data)
let s:preview_data = a:data
let l:lines = []
let l:syntax_lines = []
let l:ft = lsp#ui#vim#output#append(a:data, l:lines, l:syntax_lines)
if has_key(a:options, 'filetype')
let l:ft = a:options['filetype']
endif
let l:do_conceal = g:lsp_hover_conceal
let l:server_info = a:server !=# '' ? lsp#get_server_info(a:server) : {}
let l:config = get(l:server_info, 'config', {})
let l:do_conceal = get(l:config, 'hover_conceal', l:do_conceal)
call setbufvar(winbufnr(s:winid), 'lsp_syntax_highlights', l:syntax_lines)
call setbufvar(winbufnr(s:winid), 'lsp_do_conceal', l:do_conceal)
call lsp#ui#vim#output#setcontent(s:winid, l:lines, l:ft)
let [l:bufferlines, l:maxwidth] = lsp#ui#vim#output#get_size_info(s:winid)
if s:use_preview
" Set statusline
if has_key(a:options, 'statusline')
let &l:statusline = a:options['statusline']
endif
call s:set_cursor(l:current_window_id, a:options)
endif
" Go to the previous window to adjust positioning
call win_gotoid(l:current_window_id)
echo ''
if s:winid && (s:use_vim_popup || s:use_nvim_float)
if s:use_nvim_float
" Neovim floats
call lsp#ui#vim#output#adjust_float_placement(l:bufferlines, l:maxwidth)
call s:set_cursor(l:current_window_id, a:options)
call s:add_float_closing_hooks()
elseif s:use_vim_popup
" Vim popups
call s:set_cursor(l:current_window_id, a:options)
endif
doautocmd <nomodeline> User lsp_float_opened
endif
if l:ft ==? 'markdown'
call s:import_modules()
call s:Window.do(s:winid, {->s:Markdown.apply()})
endif
if !g:lsp_preview_keep_focus
" set the focus to the preview window
call win_gotoid(s:winid)
endif
return ''
endfunction
function! s:escape_string_for_display(str) abort
return substitute(substitute(a:str, '\r\n', '\n', 'g'), '\r', '\n', 'g')
endfunction
function! lsp#ui#vim#output#append(data, lines, syntax_lines) abort
if type(a:data) == type([])
for l:entry in a:data
call lsp#ui#vim#output#append(l:entry, a:lines, a:syntax_lines)
endfor
return 'markdown'
elseif type(a:data) ==# type('')
call extend(a:lines, split(s:escape_string_for_display(a:data), "\n", v:true))
return 'markdown'
elseif type(a:data) ==# type({}) && has_key(a:data, 'language')
let l:new_lines = split(s:escape_string_for_display(a:data.value), '\n')
let l:i = 1
while l:i <= len(l:new_lines)
call add(a:syntax_lines, { 'line': len(a:lines) + l:i, 'language': a:data.language })
let l:i += 1
endwhile
call extend(a:lines, l:new_lines)
return 'markdown'
elseif type(a:data) ==# type({}) && has_key(a:data, 'kind')
if a:data.kind ==? 'markdown'
call s:import_modules()
let l:detail = s:MarkupContent.normalize(a:data.value, {
\ 'compact': !g:lsp_preview_fixup_conceal
\ })
call extend(a:lines, s:Text.split_by_eol(l:detail))
else
call extend(a:lines, split(s:escape_string_for_display(a:data.value), '\n', v:true))
endif
return a:data.kind ==? 'plaintext' ? 'text' : a:data.kind
endif
endfunction
function! s:is_cmdwin() abort
return getcmdwintype() !=# ''
endfunction

View File

@@ -0,0 +1,160 @@
" vint: -ProhibitUnusedVariable
let s:debounce_timer_id = 0
function! s:not_supported(what) abort
return lsp#utils#error(a:what.' not supported for '.&filetype)
endfunction
function! lsp#ui#vim#signature_help#get_signature_help_under_cursor() abort
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_signature_help_provider(v:val)')
if len(l:servers) == 0
call s:not_supported('Retrieving signature help')
return
endif
let l:position = lsp#get_position()
for l:server in l:servers
call lsp#send_request(l:server, {
\ 'method': 'textDocument/signatureHelp',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'position': l:position,
\ },
\ 'on_notification': function('s:handle_signature_help', [l:server]),
\ })
endfor
call lsp#log('Retrieving signature help')
return
endfunction
function! s:handle_signature_help(server, data) abort
if lsp#client#is_error(a:data['response'])
call lsp#utils#error('Failed to retrieve signature help information for ' . a:server)
return
endif
if !has_key(a:data['response'], 'result')
return
endif
if !empty(a:data['response']['result']) && !empty(a:data['response']['result']['signatures'])
" Get current signature.
let l:signatures = get(a:data['response']['result'], 'signatures', [])
let l:signature_index = get(a:data['response']['result'], 'activeSignature', 0)
let l:signature = get(l:signatures, l:signature_index, {})
if empty(l:signature)
return
endif
" Signature label.
let l:label = l:signature['label']
" Mark current parameter.
if has_key(a:data['response']['result'], 'activeParameter')
let l:parameters = get(l:signature, 'parameters', [])
let l:parameter_index = a:data['response']['result']['activeParameter']
let l:parameter = get(l:parameters, l:parameter_index, {})
let l:parameter_label = s:get_parameter_label(l:signature, l:parameter)
if !empty(l:parameter_label)
let l:label = substitute(l:label, '\V\(' . escape(l:parameter_label, '\/?') . '\)', '`\1`', 'g')
endif
endif
let l:contents = [l:label]
if exists('l:parameter')
let l:parameter_doc = s:get_parameter_doc(l:parameter)
if !empty(l:parameter_doc)
call add(l:contents, '')
call add(l:contents, l:parameter_doc)
call add(l:contents, '')
endif
endif
if has_key(l:signature, 'documentation')
call add(l:contents, l:signature['documentation'])
endif
call lsp#ui#vim#output#preview(a:server, l:contents, {'statusline': ' LSP SignatureHelp'})
return
else
" signature help is used while inserting. So this must be graceful.
"call lsp#utils#error('No signature help information found')
endif
endfunction
function! s:get_parameter_label(signature, parameter) abort
if has_key(a:parameter, 'label')
if type(a:parameter['label']) == type([])
let l:string_range = a:parameter['label']
return strcharpart(
\ a:signature['label'],
\ l:string_range[0],
\ l:string_range[1] - l:string_range[0])
endif
return a:parameter['label']
endif
return ''
endfunction
function! s:get_parameter_doc(parameter) abort
if !has_key(a:parameter, 'documentation')
return ''
endif
let l:doc = copy(a:parameter['documentation'])
if type(l:doc) == type({})
let l:doc['value'] = printf('***%s*** - %s', a:parameter['label'], l:doc['value'])
return l:doc
endif
return printf('***%s*** - %s', a:parameter['label'], l:doc)
endfunction
function! s:on_cursor_moved() abort
let l:bufnr = bufnr('%')
call timer_stop(s:debounce_timer_id)
if g:lsp_signature_help_enabled
let s:debounce_timer_id = timer_start(g:lsp_signature_help_delay, function('s:on_text_changed_after', [l:bufnr]), { 'repeat': 1 })
endif
endfunction
function! s:on_text_changed_after(bufnr, timer) abort
if bufnr('%') != a:bufnr
return
endif
if index(['i', 's'], mode()[0]) == -1
return
endif
if win_id2win(lsp#ui#vim#output#getpreviewwinid()) >= 1
return
endif
" Cache trigger chars since this loop is heavy
let l:chars = get(b:, 'lsp_signature_help_trigger_character', [])
if empty(l:chars)
for l:server_name in lsp#get_allowed_servers(a:bufnr)
let l:chars += lsp#capabilities#get_signature_help_trigger_characters(l:server_name)
endfor
let b:lsp_signature_help_trigger_character = l:chars
endif
if index(l:chars, lsp#utils#_get_before_char_skip_white()) >= 0
call lsp#ui#vim#signature_help#get_signature_help_under_cursor()
endif
endfunction
function! lsp#ui#vim#signature_help#setup() abort
augroup _lsp_signature_help_
autocmd!
autocmd CursorMoved,CursorMovedI * call s:on_cursor_moved()
augroup END
endfunction
function! lsp#ui#vim#signature_help#_disable() abort
augroup _lsp_signature_help_
autocmd!
augroup END
endfunction

View File

@@ -0,0 +1,164 @@
let s:default_symbol_kinds = {
\ '1': 'file',
\ '2': 'module',
\ '3': 'namespace',
\ '4': 'package',
\ '5': 'class',
\ '6': 'method',
\ '7': 'property',
\ '8': 'field',
\ '9': 'constructor',
\ '10': 'enum',
\ '11': 'interface',
\ '12': 'function',
\ '13': 'variable',
\ '14': 'constant',
\ '15': 'string',
\ '16': 'number',
\ '17': 'boolean',
\ '18': 'array',
\ '19': 'object',
\ '20': 'key',
\ '21': 'null',
\ '22': 'enum member',
\ '23': 'struct',
\ '24': 'event',
\ '25': 'operator',
\ '26': 'type parameter',
\ }
let s:symbol_kinds = {}
let s:diagnostic_severity = {
\ 1: 'Error',
\ 2: 'Warning',
\ 3: 'Information',
\ 4: 'Hint',
\ }
function! s:symbols_to_loc_list_children(server, path, list, symbols, depth) abort
for l:symbol in a:symbols
let [l:line, l:col] = lsp#utils#position#lsp_to_vim(a:path, l:symbol['range']['start'])
call add(a:list, {
\ 'filename': a:path,
\ 'lnum': l:line,
\ 'col': l:col,
\ 'text': lsp#ui#vim#utils#_get_symbol_text_from_kind(a:server, l:symbol['kind']) . ' : ' . printf('%' . a:depth. 's', ' ') . l:symbol['name'],
\ })
if has_key(l:symbol, 'children') && !empty(l:symbol['children'])
call s:symbols_to_loc_list_children(a:server, a:path, a:list, l:symbol['children'], a:depth + 1)
endif
endfor
endfunction
function! lsp#ui#vim#utils#symbols_to_loc_list(server, result) abort
if !has_key(a:result['response'], 'result')
return []
endif
let l:list = []
let l:locations = type(a:result['response']['result']) == type({}) ? [a:result['response']['result']] : a:result['response']['result']
if !empty(l:locations) " some servers also return null so check to make sure it isn't empty
for l:symbol in a:result['response']['result']
if has_key(l:symbol, 'location')
let l:location = l:symbol['location']
if lsp#utils#is_file_uri(l:location['uri'])
let l:path = lsp#utils#uri_to_path(l:location['uri'])
let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:location['range']['start'])
call add(l:list, {
\ 'filename': l:path,
\ 'lnum': l:line,
\ 'col': l:col,
\ 'text': lsp#ui#vim#utils#_get_symbol_text_from_kind(a:server, l:symbol['kind']) . ' : ' . l:symbol['name'],
\ })
endif
else
let l:location = a:result['request']['params']['textDocument']['uri']
if lsp#utils#is_file_uri(l:location)
let l:path = lsp#utils#uri_to_path(l:location)
let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:symbol['range']['start'])
call add(l:list, {
\ 'filename': l:path,
\ 'lnum': l:line,
\ 'col': l:col,
\ 'text': lsp#ui#vim#utils#_get_symbol_text_from_kind(a:server, l:symbol['kind']) . ' : ' . l:symbol['name'],
\ })
if has_key(l:symbol, 'children') && !empty(l:symbol['children'])
call s:symbols_to_loc_list_children(a:server, l:path, l:list, l:symbol['children'], 1)
endif
endif
endif
endfor
endif
return l:list
endfunction
function! lsp#ui#vim#utils#diagnostics_to_loc_list(result) abort
if !has_key(a:result['response'], 'params')
return
endif
let l:uri = a:result['response']['params']['uri']
let l:diagnostics = lsp#utils#iteratable(a:result['response']['params']['diagnostics'])
let l:list = []
if !empty(l:diagnostics) && lsp#utils#is_file_uri(l:uri)
let l:path = lsp#utils#uri_to_path(l:uri)
for l:item in l:diagnostics
let l:severity_text = ''
if has_key(l:item, 'severity') && !empty(l:item['severity'])
let l:severity_text = s:get_diagnostic_severity_text(l:item['severity'])
endif
let l:text = ''
if has_key(l:item, 'source') && !empty(l:item['source'])
let l:text .= l:item['source'] . ':'
endif
if l:severity_text !=# ''
let l:text .= l:severity_text . ':'
endif
if has_key(l:item, 'code') && !empty(l:item['code'])
let l:text .= l:item['code'] . ':'
endif
let l:text .= l:item['message']
let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:item['range']['start'])
let l:location_item = {
\ 'filename': l:path,
\ 'lnum': l:line,
\ 'col': l:col,
\ 'text': l:text,
\ }
if l:severity_text !=# ''
" 'E' for error, 'W' for warning, 'I' for information, 'H' for hint
let l:location_item['type'] = l:severity_text[0]
endif
call add(l:list, l:location_item)
endfor
endif
return l:list
endfunction
function! lsp#ui#vim#utils#_get_symbol_text_from_kind(server, kind) abort
if !has_key(s:symbol_kinds, a:server)
let l:server_info = lsp#get_server_info(a:server)
if has_key (l:server_info, 'config') && has_key(l:server_info['config'], 'symbol_kinds')
let s:symbol_kinds[a:server] = extend(copy(s:default_symbol_kinds), l:server_info['config']['symbol_kinds'])
else
let s:symbol_kinds[a:server] = s:default_symbol_kinds
endif
endif
return get(s:symbol_kinds[a:server], a:kind, 'unknown symbol ' . a:kind)
endfunction
function! lsp#ui#vim#utils#get_symbol_kinds() abort
return map(keys(s:default_symbol_kinds), {idx, key -> str2nr(key)})
endfunction
function! s:get_diagnostic_severity_text(severity) abort
return s:diagnostic_severity[a:severity]
endfunction