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 @@
" https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
let s:enabled = 0
let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown')
let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent')
let s:FloatingWindow = vital#lsp#import('VS.Vim.Window.FloatingWindow')
let s:Window = vital#lsp#import('VS.Vim.Window')
let s:Buffer = vital#lsp#import('VS.Vim.Buffer')
function! lsp#internal#completion#documentation#_enable() abort
" don't even bother registering if the feature is disabled
if !g:lsp_completion_documentation_enabled | return | endif
if !s:FloatingWindow.is_available() | return | endif
if !exists('##CompleteChanged') | return | endif
if s:enabled | return | endif
let s:enabled = 1
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent('CompleteChanged'),
\ lsp#callbag#filter({_->g:lsp_completion_documentation_enabled}),
\ lsp#callbag#map({->copy(v:event)}),
\ lsp#callbag#debounceTime(g:lsp_completion_documentation_delay),
\ lsp#callbag#switchMap({event->
\ lsp#callbag#pipe(
\ s:resolve_completion(event),
\ lsp#callbag#tap({managed_user_data->s:show_floating_window(event, managed_user_data)}),
\ lsp#callbag#takeUntil(lsp#callbag#fromEvent('CompleteDone'))
\ )
\ })
\ ),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent('CompleteDone'),
\ lsp#callbag#tap({_->s:close_floating_window(v:false)}),
\ )
\ ),
\ lsp#callbag#subscribe(),
\ )
endfunction
function! s:resolve_completion(event) abort
let l:managed_user_data = lsp#omni#get_managed_user_data_from_completed_item(a:event['completed_item'])
if empty(l:managed_user_data)
return lsp#callbag#of({})
endif
let l:completion_item = l:managed_user_data['completion_item']
if has_key(l:completion_item, 'documentation')
return lsp#callbag#of(l:managed_user_data)
elseif lsp#capabilities#has_completion_resolve_provider(l:managed_user_data['server_name'])
return lsp#callbag#pipe(
\ lsp#request(l:managed_user_data['server_name'], {
\ 'method': 'completionItem/resolve',
\ 'params': l:completion_item,
\ }),
\ lsp#callbag#map({x->{
\ 'server_name': l:managed_user_data['server_name'],
\ 'completion_item': x['response']['result'],
\ 'complete_position': l:managed_user_data['complete_position'],
\ }})
\ )
else
return lsp#callbag#of(l:managed_user_data)
endif
endfunction
function! s:show_floating_window(event, managed_user_data) abort
if empty(a:managed_user_data) || !pumvisible()
call s:close_floating_window(v:true)
return
endif
let l:completion_item = a:managed_user_data['completion_item']
let l:contents = []
" Add detail field if provided.
if type(get(l:completion_item, 'detail', v:null)) == type('')
if !empty(l:completion_item.detail)
let l:detail = s:MarkupContent.normalize({
\ 'language': &filetype,
\ 'value': l:completion_item['detail'],
\ }, {
\ 'compact': !g:lsp_preview_fixup_conceal
\ })
let l:contents += [l:detail]
endif
endif
" Add documentation filed if provided.
let l:documentation = s:MarkupContent.normalize(get(l:completion_item, 'documentation', ''), {
\ 'compact': !g:lsp_preview_fixup_conceal
\ })
if !empty(l:documentation)
let l:contents += [l:documentation]
endif
" Ignore if contents is empty.
if empty(l:contents)
return s:close_floating_window(v:true)
endif
" Update contents.
let l:doc_win = s:get_doc_win()
call deletebufline(l:doc_win.get_bufnr(), 1, '$')
call setbufline(l:doc_win.get_bufnr(), 1, lsp#utils#_split_by_eol(join(l:contents, "\n\n")))
" Calculate layout.
if g:lsp_float_max_width >= 1
let l:maxwidth = g:lsp_float_max_width
elseif g:lsp_float_max_width == 0
let l:maxwidth = &columns
else
let l:maxwidth = float2nr(&columns * 0.4)
endif
let l:size = l:doc_win.get_size({
\ 'maxwidth': l:maxwidth,
\ 'maxheight': float2nr(&lines * 0.4),
\ })
let l:margin_right = &columns - 1 - (float2nr(a:event.col) + float2nr(a:event.width) + 1 + (a:event.scrollbar ? 1 : 0))
let l:margin_left = float2nr(a:event.col) - 3
if l:size.width < l:margin_right
" do nothing
elseif l:margin_left <= l:margin_right
let l:size.width = l:margin_right
else
let l:size.width = l:margin_left
endif
let l:pos = s:compute_position(a:event, l:size)
if empty(l:pos)
call s:close_floating_window(v:true)
return
endif
" Show popupmenu and apply markdown syntax.
call l:doc_win.open({
\ 'row': l:pos[0] + 1,
\ 'col': l:pos[1] + 1,
\ 'width': l:size.width,
\ 'height': l:size.height,
\ 'border': v:true,
\ 'topline': 1,
\ })
call s:Window.do(l:doc_win.get_winid(), { -> s:Markdown.apply() })
endfunction
function! s:close_floating_window(force) abort
" Ignore `CompleteDone` if it occurred by `complete()` because in this case, the popup menu will re-appear immediately.
let l:ctx = {}
function! l:ctx.callback(force) abort
if !pumvisible() || a:force
call s:get_doc_win().close()
endif
endfunction
call timer_start(1, { -> l:ctx.callback(a:force) })
endfunction
function! s:compute_position(event, size) abort
let l:col_if_right = a:event.col + a:event.width + 1 + (a:event.scrollbar ? 1 : 0)
let l:col_if_left = a:event.col - a:size.width - 2
if a:size.width >= (&columns - l:col_if_right)
let l:col = l:col_if_left
else
let l:col = l:col_if_right
endif
if l:col <= 0
return []
endif
if &columns <= l:col + a:size.width
return []
endif
return [a:event.row, l:col]
endfunction
function! s:get_doc_win() abort
if exists('s:doc_win')
return s:doc_win
endif
let s:doc_win = s:FloatingWindow.new({
\ 'on_opened': { -> execute('doautocmd <nomodeline> User lsp_float_opened') },
\ 'on_closed': { -> execute('doautocmd <nomodeline> User lsp_float_closed') }
\ })
call s:doc_win.set_var('&wrap', 1)
call s:doc_win.set_var('&conceallevel', 2)
noautocmd silent let l:bufnr = s:Buffer.create()
call s:doc_win.set_bufnr(l:bufnr)
call setbufvar(s:doc_win.get_bufnr(), '&buftype', 'nofile')
call setbufvar(s:doc_win.get_bufnr(), '&bufhidden', 'hide')
call setbufvar(s:doc_win.get_bufnr(), '&buflisted', 0)
call setbufvar(s:doc_win.get_bufnr(), '&swapfile', 0)
return s:doc_win
endfunction
function! lsp#internal#completion#documentation#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction

View File

@@ -0,0 +1,20 @@
function! lsp#internal#diagnostics#_enable() abort
" don't even bother registering if the feature is disabled
if !g:lsp_diagnostics_enabled | return | endif
call lsp#internal#diagnostics#state#_enable() " Needs to be the first one to register
call lsp#internal#diagnostics#echo#_enable()
call lsp#internal#diagnostics#highlights#_enable()
call lsp#internal#diagnostics#float#_enable()
call lsp#internal#diagnostics#signs#_enable()
call lsp#internal#diagnostics#virtual_text#_enable()
endfunction
function! lsp#internal#diagnostics#_disable() abort
call lsp#internal#diagnostics#echo#_disable()
call lsp#internal#diagnostics#float#_disable()
call lsp#internal#diagnostics#highlights#_disable()
call lsp#internal#diagnostics#virtual_text#_disable()
call lsp#internal#diagnostics#signs#_disable()
call lsp#internal#diagnostics#state#_disable() " Needs to be the last one to unregister
endfunction

View File

@@ -0,0 +1,40 @@
" options = {
" buffers: '1' " optional string, defaults to current buffer, '*' for all buffers
" }
function! lsp#internal#diagnostics#document_diagnostics_command#do(options) abort
if !g:lsp_diagnostics_enabled
call lsp#utils#error(':LspDocumentDiagnostics g:lsp_diagnostics_enabled must be enabled')
return
endif
let l:buffers = get(a:options, 'buffers', '')
let l:filtered_diagnostics = {}
if l:buffers ==# '*'
let l:filtered_diagnostics = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_uri_and_server()
else
let l:uri = lsp#utils#get_buffer_uri()
if !empty(l:uri)
let l:filtered_diagnostics[l:uri] = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri)
endif
endif
let l:result = []
for [l:uri, l:value] in items(l:filtered_diagnostics)
if lsp#internal#diagnostics#state#_is_enabled_for_buffer(bufnr(lsp#utils#uri_to_path(l:uri)))
for l:diagnostics in values(l:value)
let l:result += lsp#ui#vim#utils#diagnostics_to_loc_list({ 'response': l:diagnostics })
endfor
endif
endfor
if empty(l:result)
call lsp#utils#error('No diagnostics results')
return
else
call setloclist(0, l:result)
echo 'Retrieved diagnostics results'
botright lopen
endif
endfunction

View File

@@ -0,0 +1,41 @@
" internal state for whether it is enabled or not to avoid multiple subscriptions
let s:enabled = 0
function! lsp#internal#diagnostics#echo#_enable() abort
" don't even bother registering if the feature is disabled
if !g:lsp_diagnostics_echo_cursor | return | endif
if s:enabled | return | endif
let s:enabled = 1
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['CursorMoved']),
\ lsp#callbag#filter({_->g:lsp_diagnostics_echo_cursor}),
\ lsp#callbag#debounceTime(g:lsp_diagnostics_echo_delay),
\ lsp#callbag#map({_->{'bufnr': bufnr('%'), 'curpos': getcurpos()[0:2], 'changedtick': b:changedtick }}),
\ lsp#callbag#distinctUntilChanged({a,b -> a['bufnr'] == b['bufnr'] && a['curpos'] == b['curpos'] && a['changedtick'] == b['changedtick']}),
\ lsp#callbag#filter({_->mode() is# 'n'}),
\ lsp#callbag#filter({_->getbufvar(bufnr('%'), '&buftype') !=# 'terminal' }),
\ lsp#callbag#map({_->lsp#internal#diagnostics#under_cursor#get_diagnostic()}),
\ lsp#callbag#subscribe({x->s:echo(x)}),
\ )
endfunction
function! lsp#internal#diagnostics#echo#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
let s:enabled = 0
endfunction
function! s:echo(diagnostic) abort
if !empty(a:diagnostic) && has_key(a:diagnostic, 'message')
call lsp#utils#echo_with_truncation('LSP: '. substitute(a:diagnostic['message'], '\n\+', ' ', 'g'))
let s:displaying_message = 1
elseif get(s:, 'displaying_message', 0)
call lsp#utils#echo_with_truncation('')
let s:displaying_message = 0
endif
endfunction

View File

@@ -0,0 +1,26 @@
" Return first error line or v:null if there are no errors
" available.
" options = {
" 'bufnr': '', " optional
" }
function! lsp#internal#diagnostics#first_line#get_first_error_line(options) abort
let l:bufnr = get(a:options, 'bufnr', bufnr('%'))
if !lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr)
return v:null
endif
let l:uri = lsp#utils#get_buffer_uri(l:bufnr)
let l:diagnostics_by_server = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri)
let l:first_error_line = v:null
for l:diagnostics_response in values(l:diagnostics_by_server)
for l:item in lsp#utils#iteratable(l:diagnostics_response['params']['diagnostics'])
let l:severity = get(l:item, 'severity', 1)
if l:severity ==# 1 && (l:first_error_line ==# v:null || l:first_error_line ># l:item['range']['start']['line'])
let l:first_error_line = l:item['range']['start']['line']
endif
endfor
endfor
return l:first_error_line ==# v:null ? v:null : l:first_error_line + 1
endfunction

View File

@@ -0,0 +1,123 @@
" internal state for whether it is enabled or not to avoid multiple subscriptions
let s:enabled = 0
let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown')
let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent')
let s:FloatingWindow = vital#lsp#import('VS.Vim.Window.FloatingWindow')
let s:Window = vital#lsp#import('VS.Vim.Window')
let s:Buffer = vital#lsp#import('VS.Vim.Buffer')
function! lsp#internal#diagnostics#float#_enable() abort
" don't even bother registering if the feature is disabled
if !lsp#ui#vim#output#float_supported() | return | endif
if !g:lsp_diagnostics_float_cursor | return | endif
if s:enabled | return | endif
let s:enabled = 1
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#fromEvent(['CursorMoved', 'CursorHold']),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertEnter']),
\ lsp#callbag#filter({_->!g:lsp_diagnostics_float_insert_mode_enabled}),
\ lsp#callbag#tap({_->s:hide_float()}),
\ )
\ ),
\ lsp#callbag#filter({_->g:lsp_diagnostics_float_cursor}),
\ lsp#callbag#tap({_->s:hide_float()}),
\ lsp#callbag#debounceTime(g:lsp_diagnostics_float_delay),
\ lsp#callbag#map({_->{'bufnr': bufnr('%'), 'curpos': getcurpos()[0:2], 'changedtick': b:changedtick }}),
\ lsp#callbag#distinctUntilChanged({a,b -> a['bufnr'] == b['bufnr'] && a['curpos'] == b['curpos'] && a['changedtick'] == b['changedtick']}),
\ lsp#callbag#filter({_->mode() is# 'n'}),
\ lsp#callbag#filter({_->getbufvar(bufnr('%'), '&buftype') !=# 'terminal' }),
\ lsp#callbag#map({_->lsp#internal#diagnostics#under_cursor#get_diagnostic()}),
\ lsp#callbag#subscribe({x->s:show_float(x)}),
\ )
endfunction
function! lsp#internal#diagnostics#float#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
let s:enabled = 0
endfunction
function! s:show_float(diagnostic) abort
let l:doc_win = s:get_doc_win()
if !empty(a:diagnostic) && has_key(a:diagnostic, 'message')
" Update contents.
call deletebufline(l:doc_win.get_bufnr(), 1, '$')
call setbufline(l:doc_win.get_bufnr(), 1, lsp#utils#_split_by_eol(a:diagnostic['message']))
" Compute size.
if g:lsp_float_max_width >= 1
let l:maxwidth = g:lsp_float_max_width
elseif g:lsp_float_max_width == 0
let l:maxwidth = &columns
else
let l:maxwidth = float2nr(&columns * 0.4)
endif
let l:size = l:doc_win.get_size({
\ 'maxwidth': l:maxwidth,
\ 'maxheight': float2nr(&lines * 0.4),
\ })
" Compute position.
let l:pos = s:compute_position(l:size)
" Open window.
call l:doc_win.open({
\ 'row': l:pos[0],
\ 'col': l:pos[1],
\ 'width': l:size.width,
\ 'height': l:size.height,
\ 'border': v:true,
\ 'topline': 1,
\ })
else
call s:hide_float()
endif
endfunction
function! s:hide_float() abort
let l:doc_win = s:get_doc_win()
call l:doc_win.close()
endfunction
function! s:get_doc_win() abort
if exists('s:doc_win')
return s:doc_win
endif
let s:doc_win = s:FloatingWindow.new({
\ 'on_opened': { -> execute('doautocmd <nomodeline> User lsp_float_opened') },
\ 'on_closed': { -> execute('doautocmd <nomodeline> User lsp_float_closed') }
\ })
call s:doc_win.set_var('&wrap', 1)
call s:doc_win.set_var('&conceallevel', 2)
noautocmd silent let l:bufnr = s:Buffer.create()
call s:doc_win.set_bufnr(l:bufnr)
call setbufvar(s:doc_win.get_bufnr(), '&buftype', 'nofile')
call setbufvar(s:doc_win.get_bufnr(), '&bufhidden', 'hide')
call setbufvar(s:doc_win.get_bufnr(), '&buflisted', 0)
call setbufvar(s:doc_win.get_bufnr(), '&swapfile', 0)
return s:doc_win
endfunction
function! s:compute_position(size) abort
let l:pos = screenpos(0, line('.'), col('.'))
if l:pos.row == 0 && l:pos.col == 0
let l:pos = {'curscol': screencol(), 'row': screenrow()}
endif
let l:pos = [l:pos.row + 1, l:pos.curscol + 1]
if l:pos[0] + a:size.height > &lines
let l:pos[0] = l:pos[0] - a:size.height - 3
endif
if l:pos[1] + a:size.width > &columns
let l:pos[1] = l:pos[1] - a:size.width - 3
endif
return l:pos
endfunction

View File

@@ -0,0 +1,207 @@
" internal state for whether it is enabled or not to avoid multiple subscriptions
let s:enabled = 0
let s:namespace_id = '' " will be set when enabled
let s:severity_sign_names_mapping = {
\ 1: 'LspError',
\ 2: 'LspWarning',
\ 3: 'LspInformation',
\ 4: 'LspHint',
\ }
if !hlexists('LspErrorHighlight')
highlight link LspErrorHighlight Error
endif
if !hlexists('LspWarningHighlight')
highlight link LspWarningHighlight Todo
endif
if !hlexists('LspInformationHighlight')
highlight link LspInformationHighlight Normal
endif
if !hlexists('LspHintHighlight')
highlight link LspHintHighlight Normal
endif
function! lsp#internal#diagnostics#highlights#_enable() abort
" don't even bother registering if the feature is disabled
if !lsp#utils#_has_highlights() | return | endif
if !g:lsp_diagnostics_highlights_enabled | return | endif
if s:enabled | return | endif
let s:enabled = 1
if empty(s:namespace_id)
if has('nvim')
let s:namespace_id = nvim_create_namespace('vim_lsp_diagnostics_highlights')
else
let s:namespace_id = 'vim_lsp_diagnostics_highlights'
for l:severity in keys(s:severity_sign_names_mapping)
let l:hl_group = s:severity_sign_names_mapping[l:severity] . 'Highlight'
call prop_type_add(s:get_prop_type_name(l:severity),
\ {'highlight': l:hl_group, 'combine': v:true, 'priority': lsp#internal#textprop#priority('diagnostics_highlight') })
endfor
endif
endif
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'server') && has_key(x, 'response')
\ && has_key(x['response'], 'method') && x['response']['method'] ==# '$/vimlsp/lsp_diagnostics_updated'
\ && !lsp#client#is_error(x['response'])}),
\ lsp#callbag#map({x->x['response']['params']}),
\ ),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertEnter', 'InsertLeave']),
\ lsp#callbag#filter({_->!g:lsp_diagnostics_highlights_insert_mode_enabled}),
\ lsp#callbag#map({_->{ 'uri': lsp#utils#get_buffer_uri() }}),
\ ),
\ ),
\ lsp#callbag#filter({_->g:lsp_diagnostics_highlights_enabled}),
\ lsp#callbag#debounceTime(g:lsp_diagnostics_highlights_delay),
\ lsp#callbag#tap({x->s:clear_highlights(x)}),
\ lsp#callbag#tap({x->s:set_highlights(x)}),
\ lsp#callbag#subscribe(),
\ )
endfunction
function! lsp#internal#diagnostics#highlights#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
call s:clear_all_highlights()
let s:enabled = 0
endfunction
function! s:get_prop_type_name(severity) abort
return s:namespace_id . '_' . get(s:severity_sign_names_mapping, a:severity, 'LspError')
endfunction
function! s:clear_all_highlights() abort
for l:bufnr in range(1, bufnr('$'))
if bufexists(l:bufnr) && bufloaded(l:bufnr)
if has('nvim')
call nvim_buf_clear_namespace(l:bufnr, s:namespace_id, 0, -1)
else
for l:severity in keys(s:severity_sign_names_mapping)
try
" TODO: need to check for valid range before calling prop_add
" See https://github.com/prabirshrestha/vim-lsp/pull/721
silent! call prop_remove({
\ 'type': s:get_prop_type_name(l:severity),
\ 'bufnr': l:bufnr,
\ 'all': v:true })
catch
call lsp#log('diagnostics', 'clear_all_highlights', 'prop_remove', v:exception, v:throwpoint)
endtry
endfor
endif
endif
endfor
endfunction
function! s:clear_highlights(params) abort
" TODO: optimize by looking at params
call s:clear_all_highlights()
endfunction
function! s:set_highlights(params) abort
" TODO: optimize by looking at params
if !g:lsp_diagnostics_highlights_insert_mode_enabled
if mode()[0] ==# 'i' | return | endif
endif
for l:bufnr in range(1, bufnr('$'))
if lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr) && bufexists(l:bufnr) && bufloaded(l:bufnr)
let l:uri = lsp#utils#get_buffer_uri(l:bufnr)
for [l:server, l:diagnostics_response] in items(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri))
call s:place_highlights(l:server, l:diagnostics_response, l:bufnr)
endfor
endif
endfor
endfunction
function! s:place_highlights(server, diagnostics_response, bufnr) abort
" TODO: make diagnostics highlights same across vim and neovim
for l:item in lsp#utils#iteratable(a:diagnostics_response['params']['diagnostics'])
let [l:start_line, l:start_col] = lsp#utils#position#lsp_to_vim(a:bufnr, l:item['range']['start'])
let [l:end_line, l:end_col] = lsp#utils#position#lsp_to_vim(a:bufnr, l:item['range']['end'])
let l:severity = get(l:item, 'severity', 3)
let l:hl_group = get(s:severity_sign_names_mapping, l:severity, 'LspError') . 'Highlight'
if has('nvim')
for l:line in range(l:start_line, l:end_line)
if l:line == l:start_line
let l:highlight_start_col = l:start_col
else
let l:highlight_start_col = 1
endif
if l:line == l:end_line
let l:highlight_end_col = l:end_col
else
" neovim treats -1 as end of line, special handle it later
" when calling nvim_buf_add_higlight
let l:highlight_end_col = -1
endif
if l:start_line == l:end_line && l:highlight_start_col == l:highlight_end_col
" higlighting same start col and end col on same line
" doesn't work so use -1 for start col
let l:highlight_start_col -= 1
if l:highlight_start_col <= 0
let l:highlight_start_col = 1
endif
endif
call nvim_buf_add_highlight(a:bufnr, s:namespace_id, l:hl_group,
\ l:line - 1, l:highlight_start_col - 1, l:highlight_end_col == -1 ? -1 : l:highlight_end_col)
endfor
else
if l:start_line == l:end_line
try
" TODO: need to check for valid range before calling prop_add
" See https://github.com/prabirshrestha/vim-lsp/pull/721
silent! call prop_add(l:start_line, l:start_col, {
\ 'end_col': l:end_col,
\ 'bufnr': a:bufnr,
\ 'type': s:get_prop_type_name(l:severity),
\ })
catch
call lsp#log('diagnostics', 'place_highlights', 'prop_add', v:exception, v:throwpoint)
endtry
else
for l:line in range(l:start_line, l:end_line)
if l:line == l:start_line
let l:highlight_start_col = l:start_col
else
let l:highlight_start_col = 1
endif
if l:line == l:end_line
let l:highlight_end_col = l:end_col
else
let l:highlight_end_col = strlen(getbufline(a:bufnr, l:line, l:line)[0]) + 1
endif
try
" TODO: need to check for valid range before calling prop_add
" See https://github.com/prabirshrestha/vim-lsp/pull/721
silent! call prop_add(l:line, l:highlight_start_col, {
\ 'end_col': l:highlight_end_col,
\ 'bufnr': a:bufnr,
\ 'type': s:get_prop_type_name(l:severity),
\ })
catch
call lsp#log('diagnostics', 'place_highlights', 'prop_add', v:exception, v:throwpoint)
endtry
endfor
endif
endif
endfor
endfunction

View File

@@ -0,0 +1,199 @@
function! s:severity_of(diagnostic) abort
return get(a:diagnostic, 'severity', 1)
endfunction
function! lsp#internal#diagnostics#movement#_next_error(...) abort
let l:diagnostics = filter(s:get_all_buffer_diagnostics(),
\ {_, diagnostic -> s:severity_of(diagnostic) ==# 1 })
let l:options = lsp#utils#parse_command_options(a:000)
call s:next_diagnostic(l:diagnostics, l:options)
endfunction
function! lsp#internal#diagnostics#movement#_next_warning(...) abort
let l:diagnostics = filter(s:get_all_buffer_diagnostics(),
\ {_, diagnostic -> s:severity_of(diagnostic) ==# 2 })
let l:options = lsp#utils#parse_command_options(a:000)
call s:next_diagnostic(l:diagnostics, l:options)
endfunction
function! lsp#internal#diagnostics#movement#_next_diagnostics(...) abort
let l:options = lsp#utils#parse_command_options(a:000)
call s:next_diagnostic(s:get_all_buffer_diagnostics(), l:options)
endfunction
function! s:next_diagnostic(diagnostics, options) abort
if !len(a:diagnostics)
return
endif
call sort(a:diagnostics, 's:compare_diagnostics')
let l:wrap = 1
if has_key(a:options, 'wrap')
let l:wrap = a:options['wrap']
endif
let l:view = winsaveview()
let l:next_line = 0
let l:next_col = 0
for l:diagnostic in a:diagnostics
let [l:line, l:col] = lsp#utils#position#lsp_to_vim('%', l:diagnostic['range']['start'])
if l:line > l:view['lnum']
\ || (l:line == l:view['lnum'] && l:col > l:view['col'] + 1)
let l:next_line = l:line
let l:next_col = l:col - 1
break
endif
endfor
if l:next_line == 0
if !l:wrap
return
endif
" Wrap to start
let [l:next_line, l:next_col] = lsp#utils#position#lsp_to_vim('%', a:diagnostics[0]['range']['start'])
let l:next_col -= 1
endif
let l:view['lnum'] = l:next_line
let l:view['col'] = l:next_col
let l:view['topline'] = 1
let l:height = winheight(0)
let l:totalnum = line('$')
if l:totalnum > l:height
let l:half = l:height / 2
if l:totalnum - l:half < l:view['lnum']
let l:view['topline'] = l:totalnum - l:height + 1
else
let l:view['topline'] = l:view['lnum'] - l:half
endif
endif
call winrestview(l:view)
endfunction
function! lsp#internal#diagnostics#movement#_previous_error(...) abort
let l:diagnostics = filter(s:get_all_buffer_diagnostics(),
\ {_, diagnostic -> s:severity_of(diagnostic) ==# 1 })
let l:options = lsp#utils#parse_command_options(a:000)
call s:previous_diagnostic(l:diagnostics, l:options)
endfunction
function! lsp#internal#diagnostics#movement#_previous_warning(...) abort
let l:options = lsp#utils#parse_command_options(a:000)
let l:diagnostics = filter(s:get_all_buffer_diagnostics(),
\ {_, diagnostic -> s:severity_of(diagnostic) ==# 2 })
call s:previous_diagnostic(l:diagnostics, l:options)
endfunction
function! lsp#internal#diagnostics#movement#_previous_diagnostics(...) abort
let l:options = lsp#utils#parse_command_options(a:000)
call s:previous_diagnostic(s:get_all_buffer_diagnostics(), l:options)
endfunction
function! s:previous_diagnostic(diagnostics, options) abort
if !len(a:diagnostics)
return
endif
call sort(a:diagnostics, 's:compare_diagnostics')
let l:wrap = 1
if has_key(a:options, 'wrap')
let l:wrap = a:options['wrap']
endif
let l:view = winsaveview()
let l:next_line = 0
let l:next_col = 0
let l:index = len(a:diagnostics) - 1
while l:index >= 0
let [l:line, l:col] = lsp#utils#position#lsp_to_vim('%', a:diagnostics[l:index]['range']['start'])
if l:line < l:view['lnum']
\ || (l:line == l:view['lnum'] && l:col < l:view['col'])
let l:next_line = l:line
let l:next_col = l:col - 1
break
endif
let l:index = l:index - 1
endwhile
if l:next_line == 0
if !l:wrap
return
endif
" Wrap to end
let [l:next_line, l:next_col] = lsp#utils#position#lsp_to_vim('%', a:diagnostics[-1]['range']['start'])
let l:next_col -= 1
endif
let l:view['lnum'] = l:next_line
let l:view['col'] = l:next_col
let l:view['topline'] = 1
let l:height = winheight(0)
let l:totalnum = line('$')
if l:totalnum > l:height
let l:half = l:height / 2
if l:totalnum - l:half < l:view['lnum']
let l:view['topline'] = l:totalnum - l:height + 1
else
let l:view['topline'] = l:view['lnum'] - l:half
endif
endif
call winrestview(l:view)
endfunction
function! s:get_diagnostics(uri) abort
if has_key(s:diagnostics, a:uri)
return [1, s:diagnostics[a:uri]]
else
if s:is_win
" vim in windows always uses upper case for drive letter, so use lowercase in case lang server uses lowercase
" https://github.com/theia-ide/typescript-language-server/issues/23
let l:uri = substitute(a:uri, '^' . a:uri[:8], tolower(a:uri[:8]), '')
if has_key(s:diagnostics, l:uri)
return [1, s:diagnostics[l:uri]]
endif
endif
endif
return [0, {}]
endfunction
" Get diagnostics for the current buffer URI from all servers
function! s:get_all_buffer_diagnostics(...) abort
let l:server = get(a:000, 0, '')
let l:bufnr = bufnr('%')
let l:uri = lsp#utils#get_buffer_uri(l:bufnr)
if !lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr)
return []
endif
let l:diagnostics_by_server = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri)
let l:diagnostics = []
if empty(l:server)
for l:item in values(l:diagnostics_by_server)
let l:diagnostics += lsp#utils#iteratable(l:item['params']['diagnostics'])
endfor
else
if has_key(l:diagnostics_by_server, l:server)
let l:diagnostics = lsp#utils#iteratable(l:diagnostics_by_server[l:server]['params']['diagnostics'])
endif
endif
return l:diagnostics
endfunction
function! s:compare_diagnostics(d1, d2) abort
let l:range1 = a:d1['range']
let l:line1 = l:range1['start']['line'] + 1
let l:col1 = l:range1['start']['character'] + 1
let l:range2 = a:d2['range']
let l:line2 = l:range2['start']['line'] + 1
let l:col2 = l:range2['start']['character'] + 1
if l:line1 == l:line2
return l:col1 == l:col2 ? 0 : l:col1 > l:col2 ? 1 : -1
else
return l:line1 > l:line2 ? 1 : -1
endif
endfunction
" vim sw=4 ts=4 et

View File

@@ -0,0 +1,151 @@
" internal state for whether it is enabled or not to avoid multiple subscriptions
let s:enabled = 0
let s:sign_group = 'vim_lsp'
let s:severity_sign_names_mapping = {
\ 1: 'LspError',
\ 2: 'LspWarning',
\ 3: 'LspInformation',
\ 4: 'LspHint',
\ }
if !hlexists('LspErrorText')
highlight link LspErrorText Error
endif
if !hlexists('LspWarningText')
highlight link LspWarningText Todo
endif
if !hlexists('LspInformationText')
highlight link LspInformationText Normal
endif
if !hlexists('LspHintText')
highlight link LspHintText Normal
endif
function! lsp#internal#diagnostics#signs#_enable() abort
" don't even bother registering if the feature is disabled
if !lsp#utils#_has_signs() | return | endif
if !g:lsp_diagnostics_signs_enabled | return | endif
if s:enabled | return | endif
let s:enabled = 1
call s:define_sign('LspError', 'E>', g:lsp_diagnostics_signs_error)
call s:define_sign('LspWarning', 'W>', g:lsp_diagnostics_signs_warning)
call s:define_sign('LspInformation', 'I>', g:lsp_diagnostics_signs_information)
call s:define_sign('LspHint', 'H>', g:lsp_diagnostics_signs_hint)
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'server') && has_key(x, 'response')
\ && has_key(x['response'], 'method') && x['response']['method'] ==# '$/vimlsp/lsp_diagnostics_updated'
\ && !lsp#client#is_error(x['response'])}),
\ lsp#callbag#map({x->x['response']['params']}),
\ ),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertEnter', 'InsertLeave']),
\ lsp#callbag#filter({_->!g:lsp_diagnostics_signs_insert_mode_enabled}),
\ lsp#callbag#map({_->{ 'uri': lsp#utils#get_buffer_uri() }}),
\ ),
\ ),
\ lsp#callbag#filter({_->g:lsp_diagnostics_signs_enabled}),
\ lsp#callbag#debounceTime(g:lsp_diagnostics_signs_delay),
\ lsp#callbag#tap({x->s:clear_signs(x)}),
\ lsp#callbag#tap({x->s:set_signs(x)}),
\ lsp#callbag#subscribe(),
\ )
endfunction
function! lsp#internal#diagnostics#signs#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
call s:clear_all_signs()
call s:undefine_signs()
let s:enabled = 0
endfunction
" Set default sign text to handle case when user provides empty dict
function! s:define_sign(sign_name, sign_default_text, sign_options) abort
let l:options = {
\ 'text': get(a:sign_options, 'text', a:sign_default_text),
\ 'texthl': a:sign_name . 'Text',
\ 'linehl': a:sign_name . 'Line',
\ }
let l:sign_icon = get(a:sign_options, 'icon', '')
if !empty(l:sign_icon)
let l:options['icon'] = l:sign_icon
endif
call sign_define(a:sign_name, l:options)
endfunction
function! s:undefine_signs() abort
call sign_undefine('LspError')
call sign_undefine('LspWarning')
call sign_undefine('LspInformation')
call sign_undefine('LspHint')
endfunction
function! s:clear_all_signs() abort
call sign_unplace(s:sign_group)
endfunction
" params => {
" server: '' " optional
" uri: '' " optional
" }
function! s:clear_signs(params) abort
" TODO: optimize by looking at params
call s:clear_all_signs()
endfunction
" params => {
" server: '' " optional
" uri: '' " optional
" }
function! s:set_signs(params) abort
" TODO: optimize by looking at params
if !g:lsp_diagnostics_signs_insert_mode_enabled
if mode()[0] ==# 'i' | return | endif
endif
for l:bufnr in range(1, bufnr('$'))
if lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr) && bufexists(l:bufnr) && bufloaded(l:bufnr)
let l:uri = lsp#utils#get_buffer_uri(l:bufnr)
for [l:server, l:diagnostics_response] in items(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri))
call s:place_signs(l:server, l:diagnostics_response, l:bufnr)
endfor
endif
endfor
endfunction
function! s:place_signs(server, diagnostics_response, bufnr) abort
for l:item in lsp#utils#iteratable(a:diagnostics_response['params']['diagnostics'])
let l:line = lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['range']['start'])
" Some language servers report an unexpected EOF one line past the end
" key 'linecount' may be missing.
if has_key(getbufinfo(a:bufnr)[0], 'linecount')
if l:line == getbufinfo(a:bufnr)[0].linecount + 1
let l:line = l:line - 1
endif
endif
if has_key(l:item, 'severity') && !empty(l:item['severity'])
let l:sign_name = get(s:severity_sign_names_mapping, l:item['severity'], 'LspError')
let l:sign_priority = get(g:lsp_diagnostics_signs_priority_map, l:sign_name, g:lsp_diagnostics_signs_priority)
let l:sign_priority = get(g:lsp_diagnostics_signs_priority_map,
\ a:server . '_' . l:sign_name, l:sign_priority)
" pass 0 and let vim generate sign id
let l:sign_id = sign_place(0, s:sign_group, l:sign_name, a:bufnr,
\{ 'lnum': l:line, 'priority': l:sign_priority })
endif
endfor
endfunction

View File

@@ -0,0 +1,173 @@
" https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
"
" Refer to https://github.com/microsoft/language-server-protocol/pull/1019 on normalization of urls.
" {
" 'normalized_uri': {
" 'server_name': {
" 'method': 'textDocument/publishDiagnostics',
" 'params': {
" 'uri': 'uri', " this uri is not normalized and is exactly what server returns
" 'dignostics': [ " array can never be null but can be empty
" https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
" { range, message, severity?, code?, codeDesciption?, source?, tags?, relatedInformation?, data? }
" ]
" }
" }
" }
" Note: Do not remove when buffer unloads or doesn't exist since some server
" may send diagnsotics information regardless of textDocument/didOpen.
" buffer state is removed when server exits.
" TODO: reset buffer state when server initializes. ignoring for now for perf.
let s:diagnostics_state = {}
" internal state for whether it is enabled or not to avoid multiple subscriptions
let s:enabled = 0
let s:diagnostic_kinds = {
\ 1: 'error',
\ 2: 'warning',
\ 3: 'information',
\ 4: 'hint',
\ }
function! lsp#internal#diagnostics#state#_enable() abort
" don't even bother registering if the feature is disabled
if !g:lsp_diagnostics_enabled | return | endif
if s:enabled | return | endif
let s:enabled = 1
call lsp#internal#diagnostics#state#_reset()
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'server') && has_key(x, 'response')
\ && get(x['response'], 'method', '') ==# 'textDocument/publishDiagnostics'}),
\ lsp#callbag#tap({x->s:on_text_documentation_publish_diagnostics(x['server'], x['response'])}),
\ ),
\ lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'server') && has_key(x, 'response')
\ && get(x['response'], 'method', '') ==# '$/vimlsp/lsp_server_exit' }),
\ lsp#callbag#tap({x->s:on_exit(x['response'])}),
\ ),
\ ),
\ lsp#callbag#subscribe(),
\ )
call s:notify_diagnostics_update()
endfunction
function! lsp#internal#diagnostics#state#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
call lsp#internal#diagnostics#state#_reset()
call s:notify_diagnostics_update()
let s:enabled = 0
endfunction
function! lsp#internal#diagnostics#state#_reset() abort
let s:diagnostics_state = {}
let s:diagnostics_disabled_buffers = {}
endfunction
" callers should always treat the return value as immutable
" @return {
" 'servername': response
" }
function! lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(uri) abort
return get(s:diagnostics_state, lsp#utils#normalize_uri(a:uri), {})
endfunction
" callers should always treat the return value as immutable.
" callers should treat uri as normalized via lsp#utils#normalize_uri
" @return {
" 'normalized_uri': {
" 'servername': response
" }
" }
function! lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_uri_and_server() abort
return s:diagnostics_state
endfunction
function! s:on_text_documentation_publish_diagnostics(server, response) abort
if lsp#client#is_error(a:response) | return | endif
let l:normalized_uri = lsp#utils#normalize_uri(a:response['params']['uri'])
if !has_key(s:diagnostics_state, l:normalized_uri)
let s:diagnostics_state[l:normalized_uri] = {}
endif
let s:diagnostics_state[l:normalized_uri][a:server] = a:response
call s:notify_diagnostics_update(a:server, l:normalized_uri)
endfunction
function! s:on_exit(response) abort
let l:server = a:response['params']['server']
let l:notify = 0
for [l:key, l:value] in items(s:diagnostics_state)
if has_key(l:value, l:server)
let l:notify = 1
call remove(l:value, l:server)
endif
endfor
if l:notify | call s:notify_diagnostics_update(l:server) | endif
endfunction
function! lsp#internal#diagnostics#state#_force_notify_buffer(buffer) abort
" TODO: optimize buffer only
call s:notify_diagnostics_update()
endfunction
" call s:notify_diagnostics_update()
" call s:notify_diagnostics_update('server')
" call s:notify_diagnostics_update('server', 'uri')
function! s:notify_diagnostics_update(...) abort
let l:data = { 'server': '$vimlsp', 'response': { 'method': '$/vimlsp/lsp_diagnostics_updated', 'params': {} } }
" if a:0 > 0 | let l:data['response']['params']['server'] = a:1 | endif
" if a:0 > 1 | let l:data['response']['params']['uri'] = a:2 | endif
call lsp#stream(1, l:data)
doautocmd <nomodeline> User lsp_diagnostics_updated
endfunction
function! lsp#internal#diagnostics#state#_enable_for_buffer(bufnr) abort
if getbufvar(a:bufnr, 'lsp_diagnostics_enabled', 1) == 0
call setbufvar(a:bufnr, 'lsp_diagnostics_enabled', 1)
call s:notify_diagnostics_update()
endif
endfunction
function! lsp#internal#diagnostics#state#_disable_for_buffer(bufnr) abort
if getbufvar(a:bufnr, 'lsp_diagnostics_enabled', 1) != 0
call setbufvar(a:bufnr, 'lsp_diagnostics_enabled', 0)
call s:notify_diagnostics_update()
endif
endfunction
function! lsp#internal#diagnostics#state#_is_enabled_for_buffer(bufnr) abort
return getbufvar(a:bufnr, 'lsp_diagnostics_enabled', 1) == 1
endfunction
" Return dict with diagnostic counts for the specified buffer
" { 'error': 1, 'warning': 0, 'information': 0, 'hint': 0 }
function! lsp#internal#diagnostics#state#_get_diagnostics_count_for_buffer(bufnr) abort
let l:counts = {
\ 'error': 0,
\ 'warning': 0,
\ 'information': 0,
\ 'hint': 0,
\ }
if lsp#internal#diagnostics#state#_is_enabled_for_buffer(a:bufnr)
let l:uri = lsp#utils#get_buffer_uri(a:bufnr)
for [l:_, l:response] in items(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri))
for l:diagnostic in lsp#utils#iteratable(l:response['params']['diagnostics'])
let l:key = get(s:diagnostic_kinds, get(l:diagnostic, 'severity', 1) , 'error')
let l:counts[l:key] += 1
endfor
endfor
end
return l:counts
endfunction

View File

@@ -0,0 +1,48 @@
" Returns a diagnostic object, or empty dictionary if no diagnostics are
" available.
" options = {
" 'server': '', " optional
" }
function! lsp#internal#diagnostics#under_cursor#get_diagnostic(...) abort
let l:options = get(a:000, 0, {})
let l:server = get(l:options, 'server', '')
let l:bufnr = bufnr('%')
if !lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr)
return {}
endif
let l:uri = lsp#utils#get_buffer_uri(l:bufnr)
let l:diagnostics_by_server = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri)
let l:diagnostics = []
if empty(l:server)
for l:item in values(l:diagnostics_by_server)
let l:diagnostics += lsp#utils#iteratable(l:item['params']['diagnostics'])
endfor
else
if has_key(l:diagnostics_by_server, l:server)
let l:diagnostics = lsp#utils#iteratable(l:diagnostics_by_server[l:server]['params']['diagnostics'])
endif
endif
let l:line = line('.')
let l:col = col('.')
let l:closest_diagnostic = {}
let l:closest_distance = -1
for l:diagnostic in l:diagnostics
let [l:start_line, l:start_col] = lsp#utils#position#lsp_to_vim('%', l:diagnostic['range']['start'])
if l:line == l:start_line
let l:distance = abs(l:start_col - l:col)
if l:closest_distance < 0 || l:distance < l:closest_distance
let l:closest_diagnostic = l:diagnostic
let l:closest_distance = l:distance
endif
endif
endfor
return l:closest_diagnostic
endfunction

View File

@@ -0,0 +1,191 @@
" internal state for whether it is enabled or not to avoid multiple subscriptions
let s:enabled = 0
let s:namespace_id = '' " will be set when enabled
let s:severity_sign_names_mapping = {
\ 1: 'LspError',
\ 2: 'LspWarning',
\ 3: 'LspInformation',
\ 4: 'LspHint',
\ }
if !hlexists('LspErrorVirtualText')
if !hlexists('LspErrorText')
highlight link LspErrorVirtualText Error
else
highlight link LspErrorVirtualText LspErrorText
endif
endif
if !hlexists('LspWarningVirtualText')
if !hlexists('LspWarningText')
highlight link LspWarningVirtualText Todo
else
highlight link LspWarningVirtualText LspWarningText
endif
endif
if !hlexists('LspInformationVirtualText')
if !hlexists('LspInformationText')
highlight link LspInformationVirtualText Normal
else
highlight link LspInformationVirtualText LspInformationText
endif
endif
if !hlexists('LspHintVirtualText')
if !hlexists('LspHintText')
highlight link LspHintVirtualText Normal
else
highlight link LspHintVirtualText LspHintText
endif
endif
function! lsp#internal#diagnostics#virtual_text#_enable() abort
" don't even bother registering if the feature is disabled
if !lsp#utils#_has_nvim_virtual_text() && !lsp#utils#_has_vim_virtual_text() | return | endif
if !g:lsp_diagnostics_virtual_text_enabled | return | endif
if s:enabled | return | endif
let s:enabled = 1
if has('nvim')
if empty(s:namespace_id)
let s:namespace_id = nvim_create_namespace('vim_lsp_diagnostic_virtual_text')
endif
else
if index(prop_type_list(), 'vim_lsp_LspError_virtual_text') ==# -1
call prop_type_add('vim_lsp_LspError_virtual_text', { 'highlight': 'LspErrorVirtualText' })
call prop_type_add('vim_lsp_LspWarning_virtual_text', { 'highlight': 'LspWarningVirtualText' })
call prop_type_add('vim_lsp_LspInformation_virtual_text', { 'highlight': 'LspInformationVirtualText' })
call prop_type_add('vim_lsp_LspHint_virtual_text', { 'highlight': 'LspHintVirtualText' })
endif
endif
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'server') && has_key(x, 'response')
\ && has_key(x['response'], 'method') && x['response']['method'] ==# '$/vimlsp/lsp_diagnostics_updated'
\ && !lsp#client#is_error(x['response'])}),
\ lsp#callbag#map({x->x['response']['params']}),
\ ),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertEnter', 'InsertLeave']),
\ lsp#callbag#filter({_->!g:lsp_diagnostics_virtual_text_insert_mode_enabled}),
\ lsp#callbag#map({_->{ 'uri': lsp#utils#get_buffer_uri() }}),
\ ),
\ ),
\ lsp#callbag#filter({_->g:lsp_diagnostics_virtual_text_enabled}),
\ lsp#callbag#debounceTime(g:lsp_diagnostics_virtual_text_delay),
\ lsp#callbag#tap({x->s:clear_virtual_text(x)}),
\ lsp#callbag#tap({x->s:set_virtual_text(x)}),
\ lsp#callbag#subscribe(),
\ )
endfunction
function! lsp#internal#diagnostics#virtual_text#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
call s:clear_all_virtual_text()
let s:enabled = 0
endfunction
function! s:clear_all_virtual_text() abort
if has('nvim')
for l:bufnr in nvim_list_bufs()
if bufexists(l:bufnr) && bufloaded(l:bufnr)
call nvim_buf_clear_namespace(l:bufnr, s:namespace_id, 0, -1)
endif
endfor
else
let l:types = ['vim_lsp_LspError_virtual_text', 'vim_lsp_LspWarning_virtual_text', 'vim_lsp_LspInformation_virtual_text', 'vim_lsp_LspHint_virtual_text']
for l:bufnr in map(copy(getbufinfo()), 'v:val.bufnr')
if lsp#utils#_has_prop_remove_types()
call prop_remove({'types': l:types, 'bufnr': l:bufnr, 'all': v:true})
else
for l:type in l:types
call prop_remove({'type': l:type, 'bufnr': l:bufnr, 'all': v:true})
endfor
endif
endfor
endif
endfunction
" params => {
" server: '' " optional
" uri: '' " optional
" }
function! s:clear_virtual_text(params) abort
" TODO: optimize by looking at params
call s:clear_all_virtual_text()
endfunction
" params => {
" server: '' " optional
" uri: '' " optional
" }
function! s:set_virtual_text(params) abort
" TODO: optimize by looking at params
if !g:lsp_diagnostics_virtual_text_insert_mode_enabled
if mode()[0] ==# 'i' | return | endif
endif
if has('nvim')
for l:bufnr in nvim_list_bufs()
if lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr) && bufexists(l:bufnr) && bufloaded(l:bufnr)
let l:uri = lsp#utils#get_buffer_uri(l:bufnr)
for [l:server, l:diagnostics_response] in items(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri))
call s:place_virtual_text(l:server, l:diagnostics_response, l:bufnr)
endfor
endif
endfor
else
for l:bufnr in map(copy(getbufinfo()), 'v:val.bufnr')
if lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr) && bufexists(l:bufnr) && bufloaded(l:bufnr)
let l:uri = lsp#utils#get_buffer_uri(l:bufnr)
for [l:server, l:diagnostics_response] in items(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri))
call s:place_virtual_text(l:server, l:diagnostics_response, l:bufnr)
endfor
endif
endfor
endif
endfunction
function! s:place_virtual_text(server, diagnostics_response, bufnr) abort
for l:item in lsp#utils#iteratable(a:diagnostics_response['params']['diagnostics'])
let l:line = lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['range']['start'])
let l:name = get(s:severity_sign_names_mapping, get(l:item, 'severity', 3), 'LspError')
let l:text = g:lsp_diagnostics_virtual_text_prefix . l:item['message']
" Some language servers report an unexpected EOF one line past the end
if l:line == getbufinfo(a:bufnr)[0].linecount + 1
let l:line = l:line - 1
endif
if has('nvim')
let l:hl_name = l:name . 'VirtualText'
" need to do -1 for virtual text
call nvim_buf_set_virtual_text(a:bufnr, s:namespace_id, l:line - 1,
\ [[l:text, l:hl_name]], {})
else
" it's an error to add virtual text on lines that don't exist
" anymore due to async processing, just skip such diagnostics
if l:line <= getbufinfo(a:bufnr)[0].linecount
let l:type = 'vim_lsp_' . l:name . '_virtual_text'
call prop_remove({'all': v:true, 'type': l:type, 'bufnr': a:bufnr}, l:line)
call prop_add(
\ l:line, 0,
\ {
\ 'type': l:type, 'text': l:text, 'bufnr': a:bufnr,
\ 'text_align': g:lsp_diagnostics_virtual_text_align,
\ 'text_padding_left': g:lsp_diagnostics_virtual_text_padding_left,
\ 'text_wrap': g:lsp_diagnostics_virtual_text_wrap,
\ })
endif
endif
endfor
endfunction

View File

@@ -0,0 +1,131 @@
" https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction
" internal state for whether it is enabled or not to avoid multiple subscriptions
let s:enabled = 0
let s:sign_group = 'vim_lsp_document_code_action_signs'
if !hlexists('LspCodeActionText')
highlight link LspCodeActionText Normal
endif
function! lsp#internal#document_code_action#signs#_enable() abort
if !lsp#utils#_has_signs() | return | endif
" don't even bother registering if the feature is disabled
if !g:lsp_document_code_action_signs_enabled | return | endif
if s:enabled | return | endif
let s:enabled = 1
call s:define_sign('LspCodeAction', 'A>', g:lsp_document_code_action_signs_hint)
" Note:
" - update CodeAction signs when CusorMoved or CursorHold
" - clear signs when InsertEnter or BufLeave
" - debounce code action requests
" - automatically switch to latest code action request via switchMap()
" - cancel code action request via takeUntil() when BufLeave
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#fromEvent(['CursorMoved', 'CursorHold']),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertEnter', 'BufLeave']),
\ lsp#callbag#tap({_ -> s:clear_signs() }),
\ )
\ ),
\ lsp#callbag#filter({_ -> g:lsp_document_code_action_signs_enabled }),
\ lsp#callbag#debounceTime(g:lsp_document_code_action_signs_delay),
\ lsp#callbag#map({_->{'bufnr': bufnr('%'), 'curpos': getcurpos()[0:2], 'changedtick': b:changedtick }}),
\ lsp#callbag#distinctUntilChanged({a,b -> a['bufnr'] == b['bufnr'] && a['curpos'] == b['curpos'] && a['changedtick'] == b['changedtick']}),
\ lsp#callbag#filter({_->mode() is# 'n' && getbufvar(bufnr('%'), '&buftype') !=# 'terminal' }),
\ lsp#callbag#switchMap({_->
\ lsp#callbag#pipe(
\ s:send_request(),
\ lsp#callbag#materialize(),
\ lsp#callbag#filter({x->lsp#callbag#isNextNotification(x)}),
\ lsp#callbag#map({x->x['value']}),
\ lsp#callbag#takeUntil(
\ lsp#callbag#fromEvent('BufLeave')
\ )
\ )
\ }),
\ lsp#callbag#subscribe({x->s:set_signs(x)}),
\)
endfunction
function! lsp#internal#document_code_action#signs#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:send_request() abort
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_code_action_provider(v:val)')
if empty(l:servers)
return lsp#callbag#empty()
endif
let l:range = lsp#utils#range#_get_current_line_range()
return lsp#callbag#pipe(
\ lsp#callbag#fromList(l:servers),
\ lsp#callbag#flatMap({server->
\ lsp#request(server, {
\ 'method': 'textDocument/codeAction',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'range': l:range,
\ 'context': {
\ 'diagnostics': [],
\ 'only': ['', 'quickfix', 'refactor', 'refactor.extract', 'refactor.inline', 'refactor.rewrite'],
\ }
\ }
\ })
\ }),
\ lsp#callbag#filter({x-> !lsp#client#is_error(x['response']) && !empty(x['response']['result'])}),
\ lsp#callbag#take(1),
\ )
endfunction
function! s:clear_signs() abort
call sign_unplace(s:sign_group)
endfunction
function! s:set_signs(data) abort
call s:clear_signs()
if lsp#client#is_error(a:data['response']) | return | endif
if empty(a:data['response']['result'])
return
endif
let l:bufnr = bufnr(lsp#utils#uri_to_path(a:data['request']['params']['textDocument']['uri']))
call s:place_signs(a:data, l:bufnr)
endfunction
" Set default sign text to handle case when user provides empty dict
function! s:define_sign(sign_name, sign_default_text, sign_options) abort
let l:options = {
\ 'text': get(a:sign_options, 'text', a:sign_default_text),
\ 'texthl': a:sign_name . 'Text',
\ 'linehl': a:sign_name . 'Line',
\ }
let l:sign_icon = get(a:sign_options, 'icon', '')
if !empty(l:sign_icon)
let l:options['icon'] = l:sign_icon
endif
call sign_define(a:sign_name, l:options)
endfunction
function! s:place_signs(data, bufnr) abort
if !bufexists(a:bufnr) || !bufloaded(a:bufnr)
return
endif
let l:sign_priority = g:lsp_document_code_action_signs_priority
let l:line = lsp#utils#position#lsp_line_to_vim(a:bufnr, a:data['request']['params']['range']['start'])
let l:sign_id = sign_place(0, s:sign_group, 'LspCodeAction', a:bufnr,
\ { 'lnum': l:line, 'priority': l:sign_priority })
endfunction

View File

@@ -0,0 +1,86 @@
" options - {
" bufnr: bufnr('%') " required
" server - 'server_name' " optional
" sync: 0 " optional, defaults to 0 (async)
" }
function! lsp#internal#document_formatting#format(options) abort
let l:mode = mode()
if l:mode =~# '[vV]' || l:mode ==# "\<C-V>"
return lsp#internal#document_range_formatting#format(a:options)
endif
if has_key(a:options, 'server')
let l:servers = [a:options['server']]
else
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_formatting_provider(v:val)')
endif
if len(l:servers) == 0
let l:filetype = getbufvar(a:options['bufnr'], '&filetype')
call lsp#utils#error('textDocument/formatting not supported for ' . l:filetype)
return
endif
" TODO: ask user to select server for formatting if there are multiple servers
let l:server = l:servers[0]
redraw | echo 'Formatting Document ...'
call lsp#_new_command()
let l:request = {
\ 'method': 'textDocument/formatting',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(a:options['bufnr']),
\ 'options': {
\ 'tabSize': lsp#utils#buffer#get_indent_size(a:options['bufnr']),
\ 'insertSpaces': getbufvar(a:options['bufnr'], '&expandtab') ? v:true : v:false,
\ }
\ },
\ 'bufnr': a:options['bufnr'],
\ }
if get(a:options, 'sync', 0) == 1
try
let l:x = lsp#callbag#pipe(
\ lsp#request(l:server, l:request),
\ lsp#callbag#takeUntil(lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'command')}),
\ )),
\ lsp#callbag#toList(),
\ ).wait({ 'sleep': get(a:options, 'sleep', 1), 'timeout': get(a:options, 'timeout', g:lsp_format_sync_timeout) })
call s:format_next(l:x[0])
call s:format_complete()
catch
call s:format_error(v:exception . ' ' . v:throwpoint)
endtry
else
return lsp#callbag#pipe(
\ lsp#request(l:server, l:request),
\ lsp#callbag#takeUntil(lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'command')}),
\ )),
\ lsp#callbag#subscribe({
\ 'next':{x->s:format_next(x)},
\ 'error': {x->s:format_error(e)},
\ 'complete': {->s:format_complete()},
\ }),
\ )
endif
endfunction
function! s:format_next(x) abort
if lsp#client#is_error(a:x['response']) | return | endif
call lsp#utils#text_edit#apply_text_edits(a:x['request']['params']['textDocument']['uri'], get(a:x['response'], 'result', ''))
endfunction
function! s:format_error(e) abort
call lsp#log('Formatting Document Failed', a:e)
call lsp#utils#error('Formatting Document Failed.' . (type(a:e) == type('') ? a:e : ''))
endfunction
function! s:format_complete() abort
redraw | echo 'Formatting Document complete'
endfunction

View File

@@ -0,0 +1,238 @@
let s:use_vim_textprops = lsp#utils#_has_textprops() && !has('nvim')
let s:prop_id = 11
function! lsp#internal#document_highlight#_enable() abort
" don't event bother registering if the feature is disabled
if !g:lsp_document_highlight_enabled | return | endif
" Highlight group for references
if !hlexists('lspReference')
highlight link lspReference CursorColumn
endif
" Note:
" - update highlight references when CusorMoved or CursorHold
" - clear highlights when InsertEnter or BufLeave
" - debounce highlight requests
" - automatically switch to latest highlight request via switchMap()
" - cancel highlight request via takeUntil() when BufLeave
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#fromEvent(['CursorMoved', 'CursorHold']),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertEnter', 'BufLeave']),
\ lsp#callbag#tap({_ -> s:clear_highlights() }),
\ )
\ ),
\ lsp#callbag#filter({_ -> g:lsp_document_highlight_enabled }),
\ lsp#callbag#debounceTime(g:lsp_document_highlight_delay),
\ lsp#callbag#map({_->{'bufnr': bufnr('%'), 'curpos': getcurpos()[0:2], 'changedtick': b:changedtick }}),
\ lsp#callbag#distinctUntilChanged({a,b -> a['bufnr'] == b['bufnr'] && a['curpos'] == b['curpos'] && a['changedtick'] == b['changedtick']}),
\ lsp#callbag#filter({_->mode() is# 'n' && getbufvar(bufnr('%'), '&buftype') !=# 'terminal' }),
\ lsp#callbag#switchMap({_->
\ lsp#callbag#pipe(
\ s:send_highlight_request(),
\ lsp#callbag#materialize(),
\ lsp#callbag#filter({x->lsp#callbag#isNextNotification(x)}),
\ lsp#callbag#map({x->x['value']}),
\ lsp#callbag#takeUntil(
\ lsp#callbag#fromEvent('BufLeave')
\ )
\ )
\ }),
\ lsp#callbag#filter({_->mode() is# 'n'}),
\ lsp#callbag#subscribe({x->s:set_highlights(x)}),
\)
endfunction
function! lsp#internal#document_highlight#_disable() abort
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:send_highlight_request() abort
let l:capability = 'lsp#capabilities#has_document_highlight_provider(v:val)'
let l:servers = filter(lsp#get_allowed_servers(), l:capability)
if empty(l:servers)
return lsp#callbag#empty()
endif
return lsp#request(l:servers[0], {
\ 'method': 'textDocument/documentHighlight',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'position': lsp#get_position(),
\ },
\ })
endfunction
function! s:set_highlights(data) abort
let l:bufnr = bufnr('%')
call s:clear_highlights()
if mode() !=# 'n' | return | endif
if lsp#client#is_error(a:data['response']) | return | endif
" Get references from the response
let l:reference_list = a:data['response']['result']
if empty(l:reference_list)
return
endif
" Convert references to vim positions
let l:position_list = []
for l:reference in l:reference_list
call extend(l:position_list, lsp#utils#range#lsp_to_vim(l:bufnr, l:reference['range']))
endfor
call sort(l:position_list, function('s:compare_positions'))
" Ignore response if the cursor is not over a reference anymore
if s:in_reference(l:position_list) == -1 | return | endif
" Store references
if s:use_vim_textprops
let b:lsp_reference_positions = l:position_list
let b:lsp_reference_matches = []
else
let w:lsp_reference_positions = l:position_list
let w:lsp_reference_matches = []
endif
" Apply highlights to the buffer
call s:init_reference_highlight(l:bufnr)
if s:use_vim_textprops
for l:position in l:position_list
try
" TODO: need to check for valid range before calling prop_add
" See https://github.com/prabirshrestha/vim-lsp/pull/721
silent! call prop_add(l:position[0], l:position[1], {
\ 'id': s:prop_id,
\ 'bufnr': l:bufnr,
\ 'length': l:position[2],
\ 'type': 'vim-lsp-reference-highlight'})
call add(b:lsp_reference_matches, l:position[0])
catch
call lsp#log('document_highlight', 'set_highlights', v:exception, v:throwpoint)
endtry
endfor
else
for l:position in l:position_list
let l:match = matchaddpos('lspReference', [l:position], -5)
call add(w:lsp_reference_matches, l:match)
endfor
endif
endfunction
function! s:clear_highlights() abort
if s:use_vim_textprops
if exists('b:lsp_reference_matches')
let l:bufnr = bufnr('%')
for l:line in b:lsp_reference_matches
silent! call prop_remove(
\ {'id': s:prop_id,
\ 'bufnr': l:bufnr,
\ 'all': v:true}, l:line)
endfor
unlet b:lsp_reference_matches
unlet b:lsp_reference_positions
endif
else
if exists('w:lsp_reference_matches')
for l:match in w:lsp_reference_matches
silent! call matchdelete(l:match)
endfor
unlet w:lsp_reference_matches
unlet w:lsp_reference_positions
endif
endif
endfunction
" Compare two positions
function! s:compare_positions(p1, p2) abort
let l:line_1 = a:p1[0]
let l:line_2 = a:p2[0]
if l:line_1 != l:line_2
return l:line_1 > l:line_2 ? 1 : -1
endif
let l:col_1 = a:p1[1]
let l:col_2 = a:p2[1]
return l:col_1 - l:col_2
endfunction
" If the cursor is over a reference, return its index in
" the array. Otherwise, return -1.
function! s:in_reference(reference_list) abort
let l:line = line('.')
let l:column = col('.')
let l:index = 0
for l:position in a:reference_list
if l:line == l:position[0] &&
\ l:column >= l:position[1] &&
\ l:column < l:position[1] + l:position[2]
return l:index
endif
let l:index += 1
endfor
return -1
endfunction
function! s:init_reference_highlight(buf) abort
if s:use_vim_textprops
let l:props = {
\ 'bufnr': a:buf,
\ 'highlight': 'lspReference',
\ 'combine': v:true,
\ 'priority': lsp#internal#textprop#priority('document_highlight')
\ }
if prop_type_get('vim-lsp-reference-highlight', { 'bufnr': a:buf }) == {}
call prop_type_add('vim-lsp-reference-highlight', l:props)
endif
endif
endfunction
" Cyclically move between references by `offset` occurrences.
function! lsp#internal#document_highlight#jump(offset) abort
if s:use_vim_textprops && !exists('b:lsp_reference_positions') ||
\ !s:use_vim_textprops && !exists('w:lsp_reference_positions')
echohl WarningMsg
echom 'References not available'
echohl None
return
endif
" Get index of reference under cursor
let l:index = s:use_vim_textprops ? s:in_reference(b:lsp_reference_positions) : s:in_reference(w:lsp_reference_positions)
if l:index < 0
return
endif
let l:n = s:use_vim_textprops ? len(b:lsp_reference_positions) : len(w:lsp_reference_positions)
let l:index += a:offset
" Show a message when reaching TOP/BOTTOM of the file
if l:index < 0
echohl WarningMsg
echom 'search hit TOP, continuing at BOTTOM'
echohl None
elseif l:index >= (s:use_vim_textprops ? len(b:lsp_reference_positions) : len(w:lsp_reference_positions))
echohl WarningMsg
echom 'search hit BOTTOM, continuing at TOP'
echohl None
endif
" Wrap index
if l:index < 0 || l:index >= (s:use_vim_textprops ? len(b:lsp_reference_positions) : len(w:lsp_reference_positions))
let l:index = (l:index % l:n + l:n) % l:n
endif
" Jump
let l:target = (s:use_vim_textprops ? b:lsp_reference_positions : w:lsp_reference_positions)[l:index][0:1]
normal! m`
call cursor(l:target[0], l:target[1])
endfunction

View File

@@ -0,0 +1,278 @@
" https://microsoft.github.io/language-server-protocol/specification#textDocument_hover
let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown')
let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent')
let s:FloatingWindow = vital#lsp#import('VS.Vim.Window.FloatingWindow')
let s:Window = vital#lsp#import('VS.Vim.Window')
let s:Buffer = vital#lsp#import('VS.Vim.Buffer')
" options - {
" server - 'server_name' " optional
" ui - 'float' | 'preview'
" }
function! lsp#internal#document_hover#under_cursor#do(options) abort
let l:bufnr = bufnr('%')
let l:ui = get(a:options, 'ui', g:lsp_hover_ui)
if empty(l:ui)
let l:ui = s:FloatingWindow.is_available() ? 'float' : 'preview'
endif
if l:ui ==# 'float'
let l:doc_win = s:get_doc_win()
if l:doc_win.is_visible()
if bufnr('%') ==# l:doc_win.get_bufnr()
call s:close_floating_window()
else
call l:doc_win.enter()
inoremap <buffer><silent> <Plug>(lsp-float-close) <ESC>:<C-u>call <SID>close_floating_window()<CR>
nnoremap <buffer><silent> <Plug>(lsp-float-close) :<C-u>call <SID>close_floating_window()<CR>
execute('doautocmd <nomodeline> User lsp_float_focused')
if !hasmapto('<Plug>(lsp-float-close)')
imap <silent> <buffer> <C-c> <Plug>(lsp-float-close)
nmap <silent> <buffer> <C-c> <Plug>(lsp-float-close)
endif
endif
return
endif
endif
if has_key(a:options, 'server')
let l:servers = [a:options['server']]
else
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_hover_provider(v:val)')
endif
if len(l:servers) == 0
let l:filetype = getbufvar(l:bufnr, '&filetype')
call lsp#utils#error('textDocument/hover not supported for ' . l:filetype)
return
endif
redraw | echo 'Retrieving hover ...'
call lsp#_new_command()
" TODO: ask user to select server for formatting if there are multiple servers
let l:request = {
\ 'method': 'textDocument/hover',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'position': lsp#get_position(),
\ },
\ }
call lsp#callbag#pipe(
\ lsp#callbag#fromList(l:servers),
\ lsp#callbag#flatMap({server->
\ lsp#request(server, l:request)
\ }),
\ lsp#callbag#tap({x->s:show_hover(l:ui, x['server_name'], x['request'], x['response'])}),
\ lsp#callbag#takeUntil(lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'command')}),
\ )),
\ lsp#callbag#subscribe(),
\ )
endfunction
function! lsp#internal#document_hover#under_cursor#getpreviewwinid() abort
if exists('s:doc_win')
return s:doc_win.get_winid()
endif
return v:null
endfunction
function! s:show_hover(ui, server_name, request, response) abort
if !has_key(a:response, 'result') || empty(a:response['result']) ||
\ empty(a:response['result']['contents'])
call lsp#utils#error('No hover information found in server - ' . a:server_name)
return
endif
echo ''
if s:FloatingWindow.is_available() && a:ui ==? 'float'
call s:show_floating_window(a:server_name, a:request, a:response)
else
call s:show_preview_window(a:server_name, a:request, a:response)
endif
endfunction
function! s:show_preview_window(server_name, request, response) abort
let l:contents = s:get_contents(a:response['result']['contents'])
" Ignore if contents is empty.
if empty(l:contents)
call lsp#utils#error('Empty contents for LspHover')
return
endif
let l:lines = lsp#utils#_split_by_eol(join(l:contents, "\n\n"))
let l:view = winsaveview()
let l:alternate=@#
silent! pclose
sp LspHoverPreview
execute 'resize '.min([len(l:lines), &previewheight])
set previewwindow
setlocal conceallevel=2
setlocal bufhidden=hide
setlocal nobuflisted
setlocal buftype=nofile
setlocal noswapfile
%d _
call setline(1, l:lines)
call s:Window.do(win_getid(), {->s:Markdown.apply()})
execute "normal \<c-w>p"
call winrestview(l:view)
let @#=l:alternate
endfunction
function! s:show_floating_window(server_name, request, response) abort
call s:close_floating_window()
let l:contents = s:get_contents(a:response['result']['contents'])
" Ignore if contents is empty.
if empty(l:contents)
return s:close_floating_window()
endif
" Update contents.
let l:doc_win = s:get_doc_win()
silent! call deletebufline(l:doc_win.get_bufnr(), 1, '$')
call setbufline(l:doc_win.get_bufnr(), 1, lsp#utils#_split_by_eol(join(l:contents, "\n\n")))
" Calculate layout.
if g:lsp_float_max_width >= 1
let l:maxwidth = g:lsp_float_max_width
elseif g:lsp_float_max_width == 0
let l:maxwidth = &columns
else
let l:maxwidth = float2nr(&columns * 0.4)
endif
let l:size = l:doc_win.get_size({
\ 'maxwidth': l:maxwidth,
\ 'maxheight': float2nr(&lines * 0.4),
\ })
let l:pos = s:compute_position(l:size)
if empty(l:pos)
call s:close_floating_window()
return
endif
execute printf('augroup vim_lsp_hover_close_on_move_%d', bufnr('%'))
autocmd!
execute printf('autocmd InsertEnter,BufLeave,CursorMoved <buffer> call s:close_floating_window_on_move(%s)', getcurpos())
augroup END
" Show popupmenu and apply markdown syntax.
call l:doc_win.open({
\ 'row': l:pos[0],
\ 'col': l:pos[1],
\ 'width': l:size.width,
\ 'height': l:size.height,
\ 'border': v:true,
\ })
call s:Window.do(l:doc_win.get_winid(), { -> s:Markdown.apply() })
" Format contents to fit window
call setbufvar(l:doc_win.get_bufnr(), '&textwidth', l:size.width)
call s:Window.do(l:doc_win.get_winid(), { -> s:format_window() })
endfunction
function! s:format_window() abort
global/^/normal! gqgq
endfunction
function! s:get_contents(contents) abort
if type(a:contents) == type('')
return [a:contents]
elseif type(a:contents) == type([])
let l:result = []
for l:content in a:contents
let l:result += s:get_contents(l:content)
endfor
return l:result
elseif type(a:contents) == type({})
if has_key(a:contents, 'value')
if has_key(a:contents, 'kind')
if a:contents['kind'] ==? 'markdown'
let l:detail = s:MarkupContent.normalize(a:contents['value'], {
\ 'compact': !g:lsp_preview_fixup_conceal
\ })
return [l:detail]
else
return [a:contents['value']]
endif
elseif has_key(a:contents, 'language')
let l:detail = s:MarkupContent.normalize(a:contents, {
\ 'compact': !g:lsp_preview_fixup_conceal
\ })
return [l:detail]
else
return ''
endif
else
return ''
endif
else
return ''
endif
endfunction
function! s:close_floating_window() abort
call s:get_doc_win().close()
endfunction
function! s:close_floating_window_on_move(curpos) abort
if a:curpos != getcurpos() | call s:close_floating_window() | endif
endf
function! s:on_opened() abort
inoremap <buffer><silent> <Plug>(lsp-float-close) <ESC>:<C-u>call <SID>close_floating_window()<CR>
nnoremap <buffer><silent> <Plug>(lsp-float-close) :<C-u>call <SID>close_floating_window()<CR>
execute('doautocmd <nomodeline> User lsp_float_opened')
if !hasmapto('<Plug>(lsp-float-close)')
imap <silent> <buffer> <C-c> <Plug>(lsp-float-close)
nmap <silent> <buffer> <C-c> <Plug>(lsp-float-close)
endif
endfunction
function! s:on_closed() abort
execute('doautocmd <nomodeline> User lsp_float_closed')
endfunction
function! s:get_doc_win() abort
if exists('s:doc_win')
return s:doc_win
endif
let s:doc_win = s:FloatingWindow.new({
\ 'on_opened': function('s:on_opened'),
\ 'on_closed': function('s:on_closed')
\ })
call s:doc_win.set_var('&wrap', 1)
call s:doc_win.set_var('&conceallevel', 2)
call s:doc_win.set_bufnr(s:Buffer.create())
call setbufvar(s:doc_win.get_bufnr(), '&buftype', 'nofile')
call setbufvar(s:doc_win.get_bufnr(), '&bufhidden', 'hide')
call setbufvar(s:doc_win.get_bufnr(), '&buflisted', 0)
call setbufvar(s:doc_win.get_bufnr(), '&swapfile', 0)
return s:doc_win
endfunction
function! s:compute_position(size) abort
let l:pos = screenpos(0, line('.'), col('.'))
if l:pos.row == 0 && l:pos.col == 0
" workaround for float position
let l:pos = {'curscol': wincol(), 'row': winline()}
endif
let l:pos = [l:pos.row + 1, l:pos.curscol + 1]
if l:pos[0] + a:size.height > &lines
let l:pos[0] = l:pos[0] - a:size.height - 3
endif
if l:pos[1] + a:size.width > &columns
let l:pos[1] = l:pos[1] - a:size.width - 3
endif
return l:pos
endfunction

View File

@@ -0,0 +1,125 @@
" options - {
" bufnr: bufnr('%') " required
" type: '' " optional: defaults to visualmode(). overridden by opfunc
" server - 'server_name' " optional
" sync: 0 " optional, defaults to 0 (async)
" }
function! lsp#internal#document_range_formatting#format(options) abort
if has_key(a:options, 'server')
let l:servers = [a:options['server']]
else
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_range_formatting_provider(v:val)')
endif
if len(l:servers) == 0
let l:filetype = getbufvar(a:options['bufnr'], '&filetype')
call lsp#utils#error('textDocument/rangeFormatting not supported for ' . l:filetype)
return
endif
" TODO: ask user to select server for formatting if there are multiple servers
let l:server = l:servers[0]
redraw | echo 'Formatting Document Range ...'
call lsp#_new_command()
let [l:start_lnum, l:start_col, l:end_lnum, l:end_col] = s:get_selection_pos(get(a:options, 'type', visualmode()))
let l:start_char = lsp#utils#to_char('%', l:start_lnum, l:start_col)
let l:end_char = lsp#utils#to_char('%', l:end_lnum, l:end_col)
let l:request = {
\ 'method': 'textDocument/rangeFormatting',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(a:options['bufnr']),
\ 'range': {
\ 'start': { 'line': l:start_lnum - 1, 'character': l:start_char },
\ 'end': { 'line': l:end_lnum - 1, 'character': l:end_char },
\ },
\ 'options': {
\ 'tabSize': lsp#utils#buffer#get_indent_size(a:options['bufnr']),
\ 'insertSpaces': getbufvar(a:options['bufnr'], '&expandtab') ? v:true : v:false,
\ }
\ },
\ 'bufnr': a:options['bufnr'],
\ }
if get(a:options, 'sync', 0) == 1
try
let l:x = lsp#callbag#pipe(
\ lsp#request(l:server, l:request),
\ lsp#callbag#takeUntil(lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'command')}),
\ )),
\ lsp#callbag#toList(),
\ ).wait({ 'sleep': get(a:options, 'sleep', 1), 'timeout': get(a:options, 'timeout', g:lsp_format_sync_timeout) })
call s:format_next(l:x[0])
call s:format_complete()
catch
call s:format_error(v:exception . ' ' . v:throwpoint)
endtry
else
return lsp#callbag#pipe(
\ lsp#request(l:server, l:request),
\ lsp#callbag#takeUntil(lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'command')}),
\ )),
\ lsp#callbag#subscribe({
\ 'next':{x->s:format_next(x)},
\ 'error': {x->s:format_error(e)},
\ 'complete': {->s:format_complete()},
\ }),
\ )
endif
endfunction
function! s:format_next(x) abort
if lsp#client#is_error(a:x['response']) | return | endif
call lsp#utils#text_edit#apply_text_edits(a:x['request']['params']['textDocument']['uri'], a:x['response']['result'])
endfunction
function! s:format_error(e) abort
call lsp#log('Formatting Document Range Failed', a:e)
call lsp#utils#error('Formatting Document Range Failed.' . (type(a:e) == type('') ? a:e : ''))
endfunction
function! s:format_complete() abort
redraw | echo 'Formatting Document Range complete'
endfunction
function! s:get_selection_pos(type) abort
" TODO: support bufnr
if a:type ==? 'v'
let l:start_pos = getpos("'<")[1:2]
let l:end_pos = getpos("'>")[1:2]
" fix end_pos column (see :h getpos() and :h 'selection')
let l:end_line = getline(l:end_pos[0])
let l:offset = (&selection ==# 'inclusive' ? 1 : 2)
let l:end_pos[1] = len(l:end_line[:l:end_pos[1]-l:offset])
" edge case: single character selected with selection=exclusive
if l:start_pos[0] == l:end_pos[0] && l:start_pos[1] > l:end_pos[1]
let l:end_pos[1] = l:start_pos[1]
endif
elseif a:type ==? 'line'
let l:start_pos = [line("'["), 1]
let l:end_lnum = line("']")
let l:end_pos = [line("']"), len(getline(l:end_lnum))]
elseif a:type ==? 'char'
let l:start_pos = getpos("'[")[1:2]
let l:end_pos = getpos("']")[1:2]
else
let l:start_pos = [0, 0]
let l:end_pos = [0, 0]
endif
return l:start_pos + l:end_pos
endfunction
function! lsp#internal#document_range_formatting#opfunc(type) abort
call lsp#internal#document_range_formatting#format({
\ 'type': a:type,
\ 'bufnr': bufnr('%'),
\ })
endfunction

View File

@@ -0,0 +1,76 @@
" https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol
" options - {
" bufnr: bufnr('%') " optional
" server - 'server_name' " optional
" }
function! lsp#internal#document_symbol#search#do(options) abort
let l:bufnr = get(a:options, 'bufnr', bufnr('%'))
if has_key(a:options, 'server')
let l:servers = [a:options['server']]
else
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_symbol_provider(v:val)')
endif
if len(l:servers) == 0
let l:filetype = getbufvar(l:bufnr, '&filetype')
call lsp#utils#error('textDocument/documentSymbol not supported for ' . l:filetype)
return
endif
redraw | echo 'Retrieving document symbols ...'
call lsp#internal#ui#quickpick#open({
\ 'items': [],
\ 'busy': 1,
\ 'input': '',
\ 'key': 'text',
\ 'on_accept': function('s:on_accept'),
\ 'on_close': function('s:on_close'),
\ })
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#fromList(l:servers),
\ lsp#callbag#flatMap({server->
\ lsp#callbag#pipe(
\ lsp#request(server, {
\ 'method': 'textDocument/documentSymbol',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(l:bufnr),
\ },
\ }),
\ lsp#callbag#map({x->{'server': server, 'request': x['request'], 'response': x['response']}}),
\ )
\ }),
\ lsp#callbag#scan({acc, curr->add(acc, curr)}, []),
\ lsp#callbag#tap({x->s:update_ui_items(x)}),
\ lsp#callbag#subscribe({
\ 'complete':{->lsp#internal#ui#quickpick#busy(0)},
\ 'error':{e->s:on_error(e)},
\ }),
\ )
endfunction
function! s:update_ui_items(x) abort
let l:items = []
for l:i in a:x
let l:items += lsp#ui#vim#utils#symbols_to_loc_list(l:i['server'], l:i)
endfor
call lsp#internal#ui#quickpick#items(l:items)
endfunction
function! s:on_accept(data, ...) abort
call lsp#internal#ui#quickpick#close()
call lsp#utils#location#_open_vim_list_item(a:data['items'][0], '')
endfunction
function! s:on_close(...) abort
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:on_error(e) abort
call lsp#internal#ui#quickpick#close()
call lsp#log('LspDocumentSymbolSearch error', a:e)
endfunction

View File

@@ -0,0 +1,127 @@
let s:use_vim_textprops = lsp#utils#_has_vim_virtual_text() && !has('nvim')
function! s:set_inlay_hints(data) abort
let l:bufnr = bufnr('%')
call s:clear_inlay_hints()
if mode() !=# 'n' | return | endif
if lsp#client#is_error(a:data['response']) | return | endif
" Get hints from the response
let l:hints = a:data['response']['result']
if empty(l:hints)
return
endif
let l:not_curline = s:has_inlay_hints_mode('!curline')
for l:hint in l:hints
if l:not_curline && l:hint.position.line+1 ==# line('.')
continue
endif
let l:label = ''
if type(l:hint.label) ==# v:t_list
let l:label = join(map(copy(l:hint.label), {_,v -> v.value}), '')
else
let l:label = l:hint.label
endif
let l:text = (get(l:hint, 'paddingLeft', v:false) ? ' ' : '') . l:label . (get(l:hint, 'paddingRight', v:false) ? ' ' : '')
if !has_key(l:hint, 'kind') || l:hint.kind ==# 1
call prop_add(l:hint.position.line+1, l:hint.position.character+1, {'type': 'vim_lsp_inlay_hint_type', 'text': l:text, 'bufnr': l:bufnr})
elseif l:hint.kind ==# 2
call prop_add(l:hint.position.line+1, l:hint.position.character+1, {'type': 'vim_lsp_inlay_hint_parameter', 'text': l:text, 'bufnr': l:bufnr})
endif
endfor
endfunction
function! s:init_inlay_hints() abort
if index(prop_type_list(), 'vim_lsp_inlay_hint_type') ==# -1
call prop_type_add('vim_lsp_inlay_hint_type', { 'highlight': 'lspInlayHintsType' })
call prop_type_add('vim_lsp_inlay_hint_parameter', { 'highlight': 'lspInlayHintsParameter' })
endif
endfunction
function! lsp#internal#inlay_hints#_disable() abort
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:clear_inlay_hints() abort
let l:bufnr = bufnr('%')
call prop_remove({'type': 'vim_lsp_inlay_hint_type', 'bufnr': l:bufnr, 'all': v:true})
call prop_remove({'type': 'vim_lsp_inlay_hint_parameter', 'bufnr': l:bufnr, 'all': v:true})
endfunction
function! s:has_inlay_hints_mode(value) abort
let l:m = get(g:, 'lsp_inlay_hints_mode', {})
if type(l:m) != v:t_dict | return v:false | endif
if mode() ==# 'i'
let l:a = get(l:m, 'insert', [])
elseif mode() ==# 'n'
let l:a = get(l:m, 'normal', [])
else
return v:false
endif
if type(l:a) != v:t_list | return v:false | endif
return index(l:a, a:value) != -1 ? v:true : v:false
endfunction
function! s:send_inlay_hints_request() abort
let l:capability = 'lsp#capabilities#has_inlay_hint_provider(v:val)'
let l:servers = filter(lsp#get_allowed_servers(), l:capability)
if empty(l:servers)
return lsp#callbag#empty()
endif
if s:has_inlay_hints_mode('curline')
let l:range = lsp#utils#range#get_range_curline()
else
let l:range = lsp#utils#range#get_range()
endif
return lsp#request(l:servers[0], {
\ 'method': 'textDocument/inlayHint',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'range': l:range,
\ },
\ })
endfunction
function! lsp#internal#inlay_hints#_enable() abort
if !s:use_vim_textprops | return | endif
if !g:lsp_inlay_hints_enabled | return | endif
if !hlexists('lspInlayHintsType')
highlight link lspInlayHintsType Label
endif
if !hlexists('lspInlayHintsParameter')
highlight link lspInlayHintsParameter Todo
endif
call s:init_inlay_hints()
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#merge(
\ lsp#callbag#fromEvent(['CursorMoved', 'CursorHold']),
\ lsp#callbag#pipe(
\ lsp#callbag#fromEvent(['InsertEnter', 'BufLeave']),
\ lsp#callbag#tap({_ -> s:clear_inlay_hints() }),
\ )
\ ),
\ lsp#callbag#filter({_ -> g:lsp_inlay_hints_enabled }),
\ lsp#callbag#debounceTime(g:lsp_inlay_hints_delay),
\ lsp#callbag#filter({_->getbufvar(bufnr('%'), '&buftype') !~# '^(help\|terminal\|prompt\|popup)$'}),
\ lsp#callbag#switchMap({_->
\ lsp#callbag#pipe(
\ s:send_inlay_hints_request(),
\ lsp#callbag#materialize(),
\ lsp#callbag#filter({x->lsp#callbag#isNextNotification(x)}),
\ lsp#callbag#map({x->x['value']})
\ )
\ }),
\ lsp#callbag#subscribe({x->s:set_inlay_hints(x)}),
\)
endfunction

View File

@@ -0,0 +1,411 @@
let s:use_vim_textprops = lsp#utils#_has_textprops() && !has('nvim')
let s:use_nvim_highlight = lsp#utils#_has_nvim_buf_highlight()
let s:textprop_cache = 'vim-lsp-semantic-cache'
if s:use_nvim_highlight
let s:namespace_id = nvim_create_namespace('vim-lsp-semantic')
endif
" Global functions {{{1
function! lsp#internal#semantic#is_enabled() abort
return g:lsp_semantic_enabled && (s:use_vim_textprops || s:use_nvim_highlight) ? v:true : v:false
endfunction
function! lsp#internal#semantic#_enable() abort
if !lsp#internal#semantic#is_enabled() | return | endif
augroup lsp#internal#semantic
autocmd!
au User lsp_buffer_enabled call s:on_lsp_buffer_enabled()
augroup END
let l:events = [['User', 'lsp_buffer_enabled'], 'TextChanged', 'TextChangedI']
if exists('##TextChangedP')
call add(l:events, 'TextChangedP')
endif
let s:Dispose = lsp#callbag#pipe(
\ lsp#callbag#fromEvent(l:events),
\ lsp#callbag#filter({_->lsp#internal#semantic#is_enabled()}),
\ lsp#callbag#debounceTime(g:lsp_semantic_delay),
\ lsp#callbag#filter({_->index(['help', 'terminal', 'prompt', 'popup'], getbufvar(bufnr('%'), '&buftype')) ==# -1}),
\ lsp#callbag#filter({_->!lsp#utils#is_large_window(win_getid())}),
\ lsp#callbag#switchMap({_->
\ lsp#callbag#pipe(
\ s:semantic_request(),
\ lsp#callbag#materialize(),
\ lsp#callbag#filter({x->lsp#callbag#isNextNotification(x)}),
\ lsp#callbag#map({x->x['value']})
\ )
\ }),
\ lsp#callbag#subscribe({x->s:handle_semantic_request(x)})
\ )
endfunction
function! lsp#internal#semantic#_disable() abort
augroup lsp#internal#semantic
autocmd!
augroup END
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! lsp#internal#semantic#get_legend(server) abort
if !lsp#capabilities#has_semantic_tokens(a:server)
return {'tokenTypes': [], 'tokenModifiers': []}
endif
let l:capabilities = lsp#get_server_capabilities(a:server)
return l:capabilities['semanticTokensProvider']['legend']
endfunction
function! lsp#internal#semantic#get_token_types() abort
let l:capability = 'lsp#capabilities#has_semantic_tokens(v:val)'
let l:servers = filter(lsp#get_allowed_servers(), l:capability)
if empty(l:servers)
return []
endif
let l:legend = lsp#internal#semantic#get_legend(l:servers[0])
let l:token_types = l:legend['tokenTypes']
call map(l:token_types, {_, type -> toupper(type[0]) . type[1:]})
return l:token_types
endfunction
function! lsp#internal#semantic#get_token_modifiers() abort
let l:capability = 'lsp#capabilities#has_semantic_tokens(v:val)'
let l:servers = filter(lsp#get_allowed_servers(), l:capability)
if empty(l:servers)
return []
endif
let l:legend = lsp#internal#semantic#get_legend(l:servers[0])
let l:token_modifiers = l:legend['tokenModifiers']
call map(l:token_modifiers, {_, modifier -> toupper(modifier[0]) . modifier[1:]})
return l:token_modifiers
endfunction
function! s:on_lsp_buffer_enabled() abort
augroup lsp#internal#semantic
if !exists('#BufUnload#<buffer>')
execute 'au BufUnload <buffer> call setbufvar(' . bufnr() . ', ''lsp_semantic_previous_result_id'', '''')'
endif
augroup END
endfunction
function! s:supports_full_semantic_request(server) abort
if !lsp#capabilities#has_semantic_tokens(a:server)
return v:false
endif
let l:capabilities = lsp#get_server_capabilities(a:server)['semanticTokensProvider']
if !has_key(l:capabilities, 'full')
return v:false
endif
if type(l:capabilities['full']) ==# v:t_dict
return v:true
endif
return l:capabilities['full']
endfunction
function! s:supports_delta_semantic_request(server) abort
if !lsp#capabilities#has_semantic_tokens(a:server)
return v:false
endif
let l:capabilities = lsp#get_server_capabilities(a:server)['semanticTokensProvider']
if !has_key(l:capabilities, 'full')
return v:false
endif
if type(l:capabilities['full']) !=# v:t_dict
return v:false
endif
if !has_key(l:capabilities['full'], 'delta')
return v:false
endif
return l:capabilities['full']['delta']
endfunction
function! s:get_server() abort
let l:capability = 's:supports_delta_semantic_request(v:val)'
let l:servers = filter(lsp#get_allowed_servers(), l:capability)
if empty(l:servers)
let l:capability = 's:supports_full_semantic_request(v:val)'
let l:servers = filter(lsp#get_allowed_servers(), l:capability)
endif
if empty(l:servers)
return ''
endif
return l:servers[0]
endfunction
function! s:semantic_request() abort
let l:server = s:get_server()
if l:server ==# ''
return lsp#callbag#empty()
endif
if (s:supports_delta_semantic_request(l:server)
\ && getbufvar(bufnr(), 'lsp_semantic_previous_result_id') !=# '')
return s:delta_semantic_request(l:server)
else
return s:full_semantic_request(l:server)
endif
endfunction
function! s:full_semantic_request(server) abort
return lsp#request(a:server, {
\ 'method': 'textDocument/semanticTokens/full',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier()
\ }})
endfunction
function! s:delta_semantic_request(server) abort
return lsp#request(a:server, {
\ 'method': 'textDocument/semanticTokens/full/delta',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'previousResultId': getbufvar(bufname(), 'lsp_semantic_previous_result_id', 0)
\ }})
endfunction
" Highlight helper functions {{{1
function! s:handle_semantic_request(data) abort
if lsp#client#is_error(a:data['response'])
call lsp#log('Skipping semantic highlight: response is invalid')
return
endif
let l:server = a:data['server_name']
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)
" Skip if the buffer doesn't exist. This might happen when a buffer is
" opened and quickly deleted.
if !bufloaded(l:bufnr) | return | endif
if has_key(a:data['response']['result'], 'data')
call s:handle_semantic_tokens_response(l:server, l:bufnr, a:data['response']['result'])
elseif has_key(a:data['response']['result'], 'edits')
call s:handle_semantic_tokens_delta_response(l:server, l:bufnr, a:data['response']['result'])
else
" Don't update previous result ID if we could not update local copy
call lsp#log('SemanticHighlight: unsupported semanticTokens method')
return
endif
if has_key(a:data['response']['result'], 'resultId')
call setbufvar(l:bufnr, 'lsp_semantic_previous_result_id', a:data['response']['result']['resultId'])
endif
endfunction
function! s:handle_semantic_tokens_response(server, buf, result) abort
let l:highlights = {}
let l:legend = lsp#internal#semantic#get_legend(a:server)
for l:token in s:decode_tokens(a:result['data'])
let [l:key, l:value] = s:add_highlight(a:server, l:legend, a:buf, l:token)
let l:highlights[l:key] = get(l:highlights, l:key, []) + l:value
endfor
call s:apply_highlights(a:server, a:buf, l:highlights)
call setbufvar(a:buf, 'lsp_semantic_local_data', a:result['data'])
endfunction
function! s:startpos_compare(edit1, edit2) abort
return a:edit1[0] == a:edit2[0] ? 0 : a:edit1[0] > a:edit2[0] ? -1 : 1
endfunction
function! s:handle_semantic_tokens_delta_response(server, buf, result) abort
" The locations given in the edit are all referenced to the state before
" any are applied and sorting is not required from the server,
" therefore the edits must be sorted before applying.
let l:edits = a:result['edits']
call sort(l:edits, function('s:startpos_compare'))
let l:localdata = getbufvar(a:buf, 'lsp_semantic_local_data')
for l:edit in l:edits
let l:insertdata = get(l:edit, 'data', [])
let l:localdata = l:localdata[:l:edit['start'] - 1]
\ + l:insertdata
\ + l:localdata[l:edit['start'] + l:edit['deleteCount']:]
endfor
call setbufvar(a:buf, 'lsp_semantic_local_data', l:localdata)
let l:highlights = {}
let l:legend = lsp#internal#semantic#get_legend(a:server)
for l:token in s:decode_tokens(l:localdata)
let [l:key, l:value] = s:add_highlight(a:server, l:legend, a:buf, l:token)
let l:highlights[l:key] = get(l:highlights, l:key, []) + l:value
endfor
call s:apply_highlights(a:server, a:buf, l:highlights)
endfunction
function! s:decode_tokens(data) abort
let l:tokens = []
let l:i = 0
let l:line = 0
let l:char = 0
while l:i < len(a:data)
let l:line = l:line + a:data[l:i]
if a:data[l:i] > 0
let l:char = 0
endif
let l:char = l:char + a:data[l:i + 1]
call add(l:tokens, {
\ 'pos': {'line': l:line, 'character': l:char},
\ 'length': a:data[l:i + 2],
\ 'token_idx': a:data[l:i + 3],
\ 'token_modifiers': a:data[l:i + 4]
\ })
let l:i = l:i + 5
endwhile
return l:tokens
endfunction
function! s:clear_highlights(server, buf) abort
if s:use_vim_textprops
let l:BeginsWith = {str, prefix -> str[0:len(prefix) - 1] ==# prefix}
let l:IsSemanticTextprop = {_, textprop -> l:BeginsWith(textprop, s:textprop_type_prefix)}
let l:textprop_types = prop_type_list()
call filter(l:textprop_types, l:IsSemanticTextprop)
for l:textprop_type in l:textprop_types
silent! call prop_remove({'type': l:textprop_type, 'bufnr': a:buf, 'all': v:true})
endfor
elseif s:use_nvim_highlight
call nvim_buf_clear_namespace(a:buf, s:namespace_id, 0, line('$'))
endif
endfunction
function! s:add_highlight(server, legend, buf, token) abort
let l:startpos = lsp#utils#position#lsp_to_vim(a:buf, a:token['pos'])
let l:endpos = a:token['pos']
let l:endpos['character'] = l:endpos['character'] + a:token['length']
let l:endpos = lsp#utils#position#lsp_to_vim(a:buf, l:endpos)
if s:use_vim_textprops
let l:textprop_name = s:get_textprop_type(a:server, a:legend, a:token['token_idx'], a:token['token_modifiers'])
return [l:textprop_name, [[l:startpos[0], l:startpos[1], l:endpos[0], l:endpos[1]]]]
elseif s:use_nvim_highlight
let l:char = a:token['pos']['character']
let l:hl_name = s:get_hl_group(a:server, a:legend, a:token['token_idx'], a:token['token_modifiers'])
return [l:hl_name, [[l:startpos[0] - 1, l:startpos[1] - 1, l:endpos[1] - 1]]]
endif
endfunction
function! s:apply_highlights(server, buf, highlights) abort
call s:clear_highlights(a:server, a:buf)
if s:use_vim_textprops
for [l:type, l:prop_list] in items(a:highlights)
call prop_add_list({'type': l:type, 'bufnr': a:buf}, l:prop_list)
endfor
elseif s:use_nvim_highlight
call lsp#log(a:highlights)
for [l:hl_name, l:instances] in items(a:highlights)
for l:instance in l:instances
let [l:line, l:startcol, l:endcol] = l:instance
try
call nvim_buf_add_highlight(a:buf, s:namespace_id, l:hl_name, l:line, l:startcol, l:endcol)
catch
call lsp#log('SemanticHighlight: error while adding ' . l:hl_name . ' highlight on line ' . l:line)
endtry
endfor
endfor
end
endfunction
let s:hl_group_prefix = 'LspSemantic'
let s:default_highlight_groups = {
\ s:hl_group_prefix . 'Type': 'Type',
\ s:hl_group_prefix . 'Class': 'Type',
\ s:hl_group_prefix . 'Enum': 'Type',
\ s:hl_group_prefix . 'Interface': 'TypeDef',
\ s:hl_group_prefix . 'Struct': 'Type',
\ s:hl_group_prefix . 'TypeParameter': 'Type',
\ s:hl_group_prefix . 'Parameter': 'Identifier',
\ s:hl_group_prefix . 'Variable': 'Identifier',
\ s:hl_group_prefix . 'Property': 'Identifier',
\ s:hl_group_prefix . 'EnumMember': 'Constant',
\ s:hl_group_prefix . 'Event': 'Identifier',
\ s:hl_group_prefix . 'Function': 'Function',
\ s:hl_group_prefix . 'Method': 'Function',
\ s:hl_group_prefix . 'Macro': 'Macro',
\ s:hl_group_prefix . 'Keyword': 'Keyword',
\ s:hl_group_prefix . 'Modifier': 'Type',
\ s:hl_group_prefix . 'Comment': 'Comment',
\ s:hl_group_prefix . 'String': 'String',
\ s:hl_group_prefix . 'Number': 'Number',
\ s:hl_group_prefix . 'Regexp': 'String',
\ s:hl_group_prefix . 'Operator': 'Operator',
\ s:hl_group_prefix . 'Decorator': 'Macro'
\ }
function! s:get_hl_group(server, legend, token_idx, token_modifiers) abort
" get highlight group name
let l:Capitalise = {str -> toupper(str[0]) . str[1:]}
let l:token_name = l:Capitalise(a:legend['tokenTypes'][a:token_idx])
let l:token_modifiers = []
for l:modifier_idx in range(len(a:legend['tokenModifiers']))
" float2nr(pow(2, a)) is 1 << a
if and(a:token_modifiers, float2nr(pow(2, l:modifier_idx)))
let l:modifier_name = a:legend['tokenModifiers'][l:modifier_idx]
call add(l:token_modifiers, l:Capitalise(l:modifier_name))
endif
endfor
call sort(l:token_modifiers)
let l:hl_group = s:hl_group_prefix
\ . reduce(l:token_modifiers, {acc, val -> acc . val}, '')
\ . l:token_name
" create the highlight group if it does not already exist
if !hlexists(l:hl_group)
if has_key(s:default_highlight_groups, l:hl_group)
exec 'highlight link' l:hl_group s:default_highlight_groups[l:hl_group]
else
if a:token_modifiers != 0
let l:base_hl_group = s:get_hl_group(a:server, a:legend, a:token_idx, 0)
exec 'highlight link' l:hl_group l:base_hl_group
else
exec 'highlight link' l:hl_group 'Normal'
endif
endif
endif
return l:hl_group
endfunction
let s:textprop_type_prefix = 'vim-lsp-semantic-'
function! s:get_textprop_type(server, legend, token_idx, token_modifiers) abort
" get textprop type name
let l:textprop_type = s:textprop_type_prefix . a:server . '-' . a:token_idx . '-' . a:token_modifiers
" create the textprop type if it does not already exist
if prop_type_get(l:textprop_type) ==# {}
let l:hl_group = s:get_hl_group(a:server, a:legend, a:token_idx, a:token_modifiers)
silent! call prop_type_add(l:textprop_type, {
\ 'highlight': l:hl_group,
\ 'combine': v:true,
\ 'priority': lsp#internal#textprop#priority('semantic')})
endif
return l:textprop_type
endfunction
" vim: fdm=marker

View File

@@ -0,0 +1,74 @@
let s:ErrorType = 1
let s:WarningType = 2
let s:InfoType = 3
let s:LogType = 4
function! lsp#internal#show_message#_enable() abort
if g:lsp_show_message_log_level ==# 'none' | return | endif
let s:Dispose = lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->
\ g:lsp_show_message_log_level !=# 'none' &&
\ has_key(x, 'response') && has_key(x['response'], 'method')
\ && x['response']['method'] ==# 'window/showMessage'
\ }),
\ lsp#callbag#tap({x->s:handle_show_message(x['server'], x['response']['params'])}),
\ lsp#callbag#subscribe({ 'error': function('s:on_error') }),
\ )
endfunction
function! lsp#internal#show_message#_disable() abort
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:on_error(e) abort
call lsp#log('lsp#internal#show_message error', a:e)
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:handle_show_message(server, params) abort
let l:level = s:name_to_level(g:lsp_show_message_log_level)
let l:type = a:params['type']
if l:level < l:type
return
endif
let l:message = a:params['message']
try
if l:type == s:ErrorType
echohl ErrorMsg
elseif l:type == s:WarningType
echohl WarningMsg
endif
echom printf('%s: %s: %s', a:server, s:type_to_name(l:type), l:message)
finally
echohl None
endtry
endfunction
function! s:name_to_level(name) abort
if a:name ==# 'none'
return 0
elseif a:name ==# 'error'
return s:ErrorType
elseif a:name ==# 'warn' || a:name ==# 'warning'
return s:WarningType
elseif a:name ==# 'info'
return s:InfoType
elseif a:name ==# 'log'
return s:LogType
else
return 0
endif
endfunction
function! s:type_to_name(type) abort
return get(['unknown', 'error', 'warning', 'info', 'log'], a:type, 'unknown')
endfunction

View File

@@ -0,0 +1,58 @@
function! lsp#internal#show_message_request#_enable() abort
if !g:lsp_show_message_request_enabled | return | endif
let s:Dispose = lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->
\ g:lsp_show_message_request_enabled &&
\ has_key(x, 'request') && !has_key(x, 'response') &&
\ has_key(x['request'], 'method') && x['request']['method'] ==# 'window/showMessageRequest'
\ }),
\ lsp#callbag#map({x->s:show_message_request(x['server'], x['request'])}),
\ lsp#callbag#map({x->s:send_message_response(x['server'], x['request'], x['action'])}),
\ lsp#callbag#flatten(),
\ lsp#callbag#materialize(),
\ lsp#callbag#subscribe({ 'error': function('s:on_error') }),
\ )
endfunction
function! lsp#internal#show_message_request#_disable() abort
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:on_error(e) abort
call lsp#log('lsp#internal#show_message_request error', a:e)
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:show_message_request(server_name, request) abort
let l:params = a:request['params']
let l:selected_action = v:null
if has_key(l:params, 'actions') && !empty(l:params['actions'])
let l:options = map(copy(l:params['actions']), {i, action ->
\ printf('%d - [%s] %s', i + 1, a:server_name, action['title'])
\ })
let l:index = inputlist([l:params['message']] + l:options)
if l:index > 0 && l:index <= len(l:index)
let l:selected_action = l:params['actions'][l:index - 1]
endif
else
echom l:params['message']
endif
return { 'server': a:server_name, 'request': a:request, 'action': l:selected_action }
endfunction
function! s:send_message_response(server_name, request, action) abort
return lsp#request(a:server_name, {
\ 'id': a:request['id'],
\ 'result': a:action
\})
endfunction

View File

@@ -0,0 +1,13 @@
" TODO: currently, quickpick is generated via vim-quickpick, 'quickpick' is
" not used.
let s:priorities = {
\ 'quickpick': 1,
\ 'folding': 2,
\ 'semantic': 3,
\ 'diagnostics_highlight': 4,
\ 'document_highlight': 5,
\}
function! lsp#internal#textprop#priority(name) abort
return get(s:priorities, a:name, 0)
endfunction

View File

@@ -0,0 +1,91 @@
function! lsp#internal#type_hierarchy#show() abort
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_type_hierarchy_provider(v:val)')
let l:command_id = lsp#_new_command()
if len(l:servers) == 0
return lsp#utils#error('Retrieving type hierarchy not supported for ' . &filetype)
endif
let l:ctx = { 'counter': len(l:servers), 'list':[], 'last_command_id': l:command_id }
" direction 0 children, 1 parent, 2 both
for l:server in l:servers
call lsp#send_request(l:server, {
\ 'method': 'textDocument/typeHierarchy',
\ 'params': {
\ 'textDocument': lsp#get_text_document_identifier(),
\ 'position': lsp#get_position(),
\ 'direction': 2,
\ 'resolve': 1,
\ },
\ 'on_notification': function('s:handle_type_hierarchy', [l:ctx, l:server, 'type hierarchy']),
\ })
endfor
echo 'Retrieving type hierarchy ...'
endfunction
function! s:handle_type_hierarchy(ctx, server, type, data) abort "ctx = {counter, list, last_command_id}
if a:ctx['last_command_id'] != lsp#_last_command()
return
endif
if lsp#client#is_error(a:data['response'])
call lsp#utils#error('Failed to '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response']))
return
endif
if empty(a:data['response']['result'])
echo 'No type hierarchy found'
return
endif
" Create new buffer in a split
let l:position = 'topleft'
let l:orientation = 'new'
exec l:position . ' ' . 10 . l:orientation
let l:provider = {
\ 'root': a:data['response']['result'],
\ 'root_state': 'expanded',
\ 'bufnr': bufnr('%'),
\ 'getChildren': function('s:get_children_for_tree_hierarchy'),
\ 'getParent': function('s:get_parent_for_tree_hierarchy'),
\ 'getTreeItem': function('s:get_treeitem_for_tree_hierarchy'),
\ }
call lsp#utils#tree#new(l:provider)
echo 'Retrieved type hierarchy'
endfunction
function! s:hierarchyitem_to_treeitem(hierarchyitem) abort
return {
\ 'id': a:hierarchyitem,
\ 'label': a:hierarchyitem['name'],
\ 'command': function('s:hierarchy_treeitem_command', [a:hierarchyitem]),
\ 'collapsibleState': has_key(a:hierarchyitem, 'parents') && !empty(a:hierarchyitem['parents']) ? 'expanded' : 'none',
\ }
endfunction
function! s:hierarchy_treeitem_command(hierarchyitem) abort
bwipeout
call lsp#utils#tagstack#_update()
call lsp#utils#location#_open_lsp_location(a:hierarchyitem)
endfunction
function! s:get_children_for_tree_hierarchy(Callback, ...) dict abort
if a:0 == 0
call a:Callback('success', [l:self['root']])
return
else
call a:Callback('success', a:1['parents'])
endif
endfunction
function! s:get_parent_for_tree_hierarchy(...) dict abort
" TODO
endfunction
function! s:get_treeitem_for_tree_hierarchy(Callback, object) dict abort
call a:Callback('success', s:hierarchyitem_to_treeitem(a:object))
endfunction

View File

@@ -0,0 +1,39 @@
let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown')
let s:Window = vital#lsp#import('VS.Vim.Window')
function! lsp#internal#ui#popupmenu#open(opt) abort
let l:Callback = remove(a:opt, 'callback')
let l:items = remove(a:opt, 'items')
let l:items_with_shortcuts= map(l:items, {
\ idx, item -> ((idx < 9) ? '['.(idx+1).'] ' : '').item
\ })
function! Filter(id, key) abort closure
if a:key >= 1 && a:key <= len(l:items)
call popup_close(a:id, a:key)
elseif a:key ==# "\<C-j>"
call win_execute(a:id, 'normal! j')
elseif a:key ==# "\<C-k>"
call win_execute(a:id, 'normal! k')
else
return popup_filter_menu(a:id, a:key)
endif
return v:true
endfunction
let l:popup_opt = extend({
\ 'callback': funcref('s:callback', [l:Callback]),
\ 'filter': funcref('Filter'),
\ }, a:opt)
let l:winid = popup_menu(l:items_with_shortcuts, l:popup_opt)
call s:Window.do(l:winid, { -> s:Markdown.apply() })
execute('doautocmd <nomodeline> User lsp_float_opened')
endfunction
function! s:callback(callback, id, selected) abort
call a:callback(a:id, a:selected)
execute('doautocmd <nomodeline> User lsp_float_closed')
endfunction

View File

@@ -0,0 +1,461 @@
" https://github.com/prabirshrestha/quickpick.vim#968f00787c1a118228aee869351e754bec555298
" :QuickpickEmbed path=autoload/lsp/internal/ui/quickpick.vim namespace=lsp#internal#ui#quickpick prefix=lsp-quickpick
let s:has_timer = exists('*timer_start') && exists('*timer_stop')
let s:has_matchfuzzy = exists('*matchfuzzy')
let s:has_matchfuzzypos = exists('*matchfuzzypos')
let s:has_proptype = exists('*prop_type_add') && exists('*prop_type_delete')
"
" is_floating
"
if has('nvim')
function! s:is_floating(winid) abort
if !s:win_exists(a:winid)
return 0
endif
let l:config = nvim_win_get_config(a:winid)
return empty(l:config) || !empty(get(l:config, 'relative', ''))
endfunction
else
function! s:is_floating(winid) abort
return s:win_exists(a:winid) && win_id2win(a:winid) == 0
endfunction
endif
function! s:win_exists(winid) abort
return winheight(a:winid) != -1
endfunction
function! lsp#internal#ui#quickpick#open(opt) abort
call lsp#internal#ui#quickpick#close() " hide existing picker if exists
" when key is empty, item is a string else it is a dict
" fitems is filtered items and is the item that is filtered
let s:state = extend({
\ 'items': [],
\ 'highlights': [],
\ 'fitems': [],
\ 'key': '',
\ 'busy': 0,
\ 'busyframes': ['-', '\', '|', '/'],
\ 'filetype': 'lsp-quickpick',
\ 'promptfiletype': 'lsp-quickpick-filter',
\ 'input': '',
\ 'maxheight': 10,
\ 'debounce': 250,
\ 'filter': 1,
\ }, a:opt)
let s:inputecharpre = 0
let s:state['busyframe'] = 0
let s:state['bufnr'] = bufnr('%')
let s:state['winid'] = win_getid()
let s:state['wininfo'] = getwininfo()
" create result buffer
exe printf('keepalt botright 3new %s', s:state['filetype'])
let s:state['resultsbufnr'] = bufnr('%')
let s:state['resultswinid'] = win_getid()
if s:has_proptype
call prop_type_add('highlight', { 'highlight': 'Directory', 'bufnr': s:state['resultsbufnr'] })
endif
" create prompt buffer
exe printf('keepalt botright 1new %s', s:state['promptfiletype'])
let s:state['promptbufnr'] = bufnr('%')
let s:state['promptwinid'] = win_getid()
call win_gotoid(s:state['resultswinid'])
call s:set_buffer_options()
setlocal cursorline
call s:update_items()
exec printf('setlocal filetype=' . s:state['filetype'])
call s:notify('open', { 'bufnr': s:state['bufnr'], 'winid': s:state['winid'] , 'resultsbufnr': s:state['resultsbufnr'], 'resultswinid': s:state['resultswinid'] })
call win_gotoid(s:state['promptwinid'])
call s:set_buffer_options()
call setline(1, s:state['input'])
" map keys
inoremap <buffer><silent> <Plug>(lsp-quickpick-accept) <ESC>:<C-u>call <SID>on_accept()<CR>
nnoremap <buffer><silent> <Plug>(lsp-quickpick-accept) :<C-u>call <SID>on_accept()<CR>
inoremap <buffer><silent> <Plug>(lsp-quickpick-close) <ESC>:<C-u>call lsp#internal#ui#quickpick#close()<CR>
nnoremap <buffer><silent> <Plug>(lsp-quickpick-close) :<C-u>call lsp#internal#ui#quickpick#close()<CR>
inoremap <buffer><silent> <Plug>(lsp-quickpick-cancel) <ESC>:<C-u>call <SID>on_cancel()<CR>
nnoremap <buffer><silent> <Plug>(lsp-quickpick-cancel) :<C-u>call <SID>on_cancel()<CR>
inoremap <buffer><silent> <Plug>(lsp-quickpick-move-next) <C-o>:<C-u>call <SID>on_move_next()<CR>
nnoremap <buffer><silent> <Plug>(lsp-quickpick-move-next) :<C-u>call <SID>on_move_next()<CR>
inoremap <buffer><silent> <Plug>(lsp-quickpick-move-previous) <C-o>:<C-u>call <SID>on_move_previous()<CR>
nnoremap <buffer><silent> <Plug>(lsp-quickpick-move-previous) :<C-u>call <SID>on_move_previous()<CR>
exec printf('setlocal filetype=' . s:state['promptfiletype'])
if !hasmapto('<Plug>(lsp-quickpick-accept)')
imap <buffer><cr> <Plug>(lsp-quickpick-accept)
nmap <buffer><cr> <Plug>(lsp-quickpick-accept)
endif
if !hasmapto('<Plug>(lsp-quickpick-cancel)')
imap <silent> <buffer> <C-c> <Plug>(lsp-quickpick-cancel)
map <silent> <buffer> <C-c> <Plug>(lsp-quickpick-cancel)
imap <silent> <buffer> <Esc> <Plug>(lsp-quickpick-cancel)
map <silent> <buffer> <Esc> <Plug>(lsp-quickpick-cancel)
endif
if !hasmapto('<Plug>(lsp-quickpick-move-next)')
imap <silent> <buffer> <C-n> <Plug>(lsp-quickpick-move-next)
nmap <silent> <buffer> <C-n> <Plug>(lsp-quickpick-move-next)
imap <silent> <buffer> <C-j> <Plug>(lsp-quickpick-move-next)
nmap <silent> <buffer> <C-j> <Plug>(lsp-quickpick-move-next)
endif
if !hasmapto('<Plug>(lsp-quickpick-move-previous)')
imap <silent> <buffer> <C-p> <Plug>(lsp-quickpick-move-previous)
nmap <silent> <buffer> <C-p> <Plug>(lsp-quickpick-move-previous)
imap <silent> <buffer> <C-k> <Plug>(lsp-quickpick-move-previous)
nmap <silent> <buffer> <C-k> <Plug>(lsp-quickpick-move-previous)
endif
call cursor(line('$'), 0)
call feedkeys('i', 'n')
augroup lsp#internal#ui#quickpick
autocmd!
autocmd InsertCharPre <buffer> call s:on_insertcharpre()
autocmd TextChangedI <buffer> call s:on_inputchanged()
autocmd InsertEnter <buffer> call s:on_insertenter()
autocmd InsertLeave <buffer> call s:on_insertleave()
if exists('##TextChangedP')
autocmd TextChangedP <buffer> call s:on_inputchanged()
endif
augroup END
call s:notify_items()
call s:notify_selection()
call lsp#internal#ui#quickpick#busy(s:state['busy'])
endfunction
function! s:set_buffer_options() abort
" set buffer options
abc <buffer>
setlocal bufhidden=unload " unload buf when no longer displayed
setlocal buftype=nofile " buffer is not related to any file<Paste>
setlocal noswapfile " don't create swap file
setlocal nowrap " don't soft-wrap
setlocal nonumber " don't show line numbers
setlocal nolist " don't use list mode (visible tabs etc)
setlocal foldcolumn=0 " don't show a fold column at side
setlocal foldlevel=99 " don't fold anything
setlocal nospell " spell checking off
setlocal nobuflisted " don't show up in the buffer list
setlocal textwidth=0 " don't hardwarp (break long lines)
setlocal nocursorline " highlight the line cursor is off
setlocal nocursorcolumn " disable cursor column
setlocal noundofile " don't enable undo
setlocal winfixheight
if exists('+colorcolumn') | setlocal colorcolumn=0 | endif
if exists('+relativenumber') | setlocal norelativenumber | endif
setlocal signcolumn=yes " for prompt
endfunction
function! lsp#internal#ui#quickpick#close() abort
if !exists('s:state')
return
endif
call lsp#internal#ui#quickpick#busy(0)
call win_gotoid(s:state['winid'])
call s:notify('close', { 'bufnr': s:state['bufnr'], 'winid': s:state['winid'], 'resultsbufnr': s:state['resultsbufnr'], 'resultswinid': s:state['winid'] })
augroup lsp#internal#ui#quickpick
autocmd!
augroup END
exe 'silent! bunload! ' . s:state['promptbufnr']
exe 'silent! bunload! ' . s:state['resultsbufnr']
call s:restore_windows()
let s:inputecharpre = 0
unlet s:state
endfunction
function! s:restore_windows() abort
let [l:tabnr, l:_] = win_id2tabwin(s:state['winid'])
if l:tabnr == 0
return
endif
let l:Resizable = {_, info ->
\ info.tabnr == l:tabnr &&
\ s:win_exists(info.winid) &&
\ !s:is_floating(info.winid)
\ }
let l:wins_to_resize = sort(filter(s:state['wininfo'], l:Resizable), {l, r -> l.winnr - r.winnr})
let l:open_winids_to_resize = map(filter(getwininfo(), l:Resizable), {_, info -> info.winid})
let l:resize_cmd = ''
for l:info in l:wins_to_resize
if index(l:open_winids_to_resize, l:info.winid) == -1
return
endif
let l:resize_cmd .= printf('%dresize %d | vert %dresize %d |', l:info.winnr, l:info.height, l:info.winnr, l:info.width)
endfor
" winrestcmd repeats :resize commands twice after patch-8.2.2631.
" To simulate this behavior, execute the :resize commands twice.
" see https://github.com/vim/vim/issues/7988
exe 'silent! ' . l:resize_cmd . l:resize_cmd
endfunction
function! lsp#internal#ui#quickpick#items(items) abort
let s:state['items'] = a:items
call s:update_items()
call s:notify_items()
call s:notify_selection()
endfunction
function! lsp#internal#ui#quickpick#busy(busy) abort
let s:state['busy'] = a:busy
if a:busy
if !has_key(s:state, 'busytimer')
let s:state['busyframe'] = 0
let s:state['busytimer'] = timer_start(60, function('s:busy_tick'), { 'repeat': -1 })
endif
else
if has_key(s:state, 'busytimer')
call timer_stop(s:state['busytimer'])
call remove(s:state, 'busytimer')
redraw
echohl None
echo ''
endif
endif
endfunction
function! lsp#internal#ui#quickpick#results_winid() abort
if exists('s:state')
return s:state['resultswinid']
else
return 0
endif
endfunction
function! s:busy_tick(...) abort
let s:state['busyframe'] = s:state['busyframe'] + 1
if s:state['busyframe'] >= len(s:state['busyframes'])
let s:state['busyframe'] = 0
endif
redraw
echohl Question | echon s:state['busyframes'][s:state['busyframe']]
echohl None
endfunction
function! s:update_items() abort
call s:win_execute(s:state['resultswinid'], 'silent! %delete _')
let s:state['highlights'] = []
if s:state['filter'] " if filter is enabled
if empty(s:trim(s:state['input']))
let s:state['fitems'] = s:state['items']
else
if empty(s:state['key']) " item is string
if s:has_matchfuzzypos
let l:matchfuzzyresult = matchfuzzypos(s:state['items'], s:state['input'])
let l:fitems = l:matchfuzzyresult[0]
let l:highlights = l:matchfuzzyresult[1]
let s:state['fitems'] = l:fitems
let s:state['highlights'] = l:highlights
elseif s:has_matchfuzzy
let s:state['fitems'] = matchfuzzy(s:state['items'], s:state['input'])
else
let s:state['fitems'] = filter(copy(s:state['items']), 'stridx(toupper(v:val), toupper(s:state["input"])) >= 0')
endif
else " item is dict
if s:has_matchfuzzypos
" vim requires matchfuzzypos to have highlights.
" matchfuzzy only patch doesn't support dict search
let l:matchfuzzyresult = matchfuzzypos(s:state['items'], s:state['input'], { 'key': s:state['key'] })
let l:fitems = l:matchfuzzyresult[0]
let l:highlights = l:matchfuzzyresult[1]
let s:state['fitems'] = l:fitems
let s:state['highlights'] = l:highlights
else
let s:state['fitems'] = filter(copy(s:state['items']), 'stridx(toupper(v:val[s:state["key"]]), toupper(s:state["input"])) >= 0')
endif
endif
endif
else " if filter is disabled
let s:state['fitems'] = s:state['items']
endif
if empty(s:state['key']) " item is string
let l:lines = s:state['fitems']
else " item is dict
let l:lines = map(copy(s:state['fitems']), 'v:val[s:state["key"]]')
endif
call setbufline(s:state['resultsbufnr'], 1, l:lines)
if s:has_proptype && !empty(s:state['highlights'])
let l:i = 0
for l:line in s:state['highlights']
for l:pos in l:line
let l:cs = split(getbufline(s:state['resultsbufnr'], l:i + 1)[0], '\zs')
let l:mpos = strlen(join(l:cs[: l:pos - 1], ''))
let l:len = strlen(l:cs[l:pos])
call prop_add(l:i + 1, l:mpos + 1, { 'length': l:len, 'type': 'highlight', 'bufnr': s:state['resultsbufnr'] })
endfor
let l:i += 1
endfor
endif
call s:win_execute(s:state['resultswinid'], printf('resize %d', min([len(s:state['fitems']), s:state['maxheight']])))
call s:win_execute(s:state['promptwinid'], 'resize 1')
endfunction
function! s:on_accept() abort
if win_gotoid(s:state['resultswinid'])
let l:index = line('.') - 1 " line is 1 index, list is 0 index
let l:fitems = s:state['fitems']
if l:index < 0 || len(l:fitems) <= l:index
let l:items = []
else
let l:items = [l:fitems[l:index]]
endif
call win_gotoid(s:state['winid'])
call s:notify('accept', { 'items': l:items })
end
endfunction
function! s:on_cancel() abort
call win_gotoid(s:state['winid'])
call s:notify('cancel', {})
call lsp#internal#ui#quickpick#close()
endfunction
function! s:on_move_next() abort
let l:col = col('.')
call s:win_execute(s:state['resultswinid'], 'normal! j')
call s:notify_selection()
endfunction
function! s:on_move_previous() abort
let l:col = col('.')
call s:win_execute(s:state['resultswinid'], 'normal! k')
call s:notify_selection()
endfunction
function! s:notify_items() abort
" items could be huge, so don't send the items as part of data
call s:notify('items', { 'bufnr': s:state['bufnr'], 'winid': s:state['winid'], 'resultsbufnr': s:state['resultsbufnr'], 'resultswinid': s:state['resultswinid'] })
endfunction
function! s:notify_selection() abort
let l:original_winid = win_getid()
call win_gotoid(s:state['resultswinid'])
let l:index = line('.') - 1 " line is 1 based, list is 0 based
if l:index < 0 || ((l:index + 1) > len(s:state['fitems']))
let l:items = []
else
let l:items = [s:state['fitems'][l:index]]
endif
let l:data = {
\ 'bufnr': s:state['bufnr'],
\ 'winid': s:state['winid'],
\ 'resultsbufnr': s:state['resultsbufnr'],
\ 'resultswinid': s:state['resultswinid'],
\ 'items': l:items,
\ }
noautocmd call win_gotoid(s:state['winid'])
call s:notify('selection', l:data)
noautocmd call win_gotoid(l:original_winid)
endfunction
function! s:on_inputchanged() abort
if s:inputecharpre
if s:has_timer && s:state['debounce'] > 0
call s:debounce_onchange()
else
call s:notify_onchange()
endif
endif
endfunction
function! s:on_insertcharpre() abort
let s:inputecharpre = 1
endfunction
function! s:on_insertenter() abort
let s:inputecharpre = 0
endfunction
function! s:on_insertleave() abort
if s:has_timer && has_key(s:state, 'debounce_onchange_timer')
call timer_stop(s:state['debounce_onchange_timer'])
call remove(s:state, 'debounce_onchange_timer')
endif
endfunction
function! s:debounce_onchange() abort
if has_key(s:state, 'debounce_onchange_timer')
call timer_stop(s:state['debounce_onchange_timer'])
call remove(s:state, 'debounce_onchange_timer')
endif
let s:state['debounce_onchange_timer'] = timer_start(s:state['debounce'], function('s:notify_onchange'))
endfunction
function! s:notify_onchange(...) abort
let s:state['input'] = getbufline(s:state['promptbufnr'], 1)[0]
call s:notify('change', { 'input': s:state['input'] })
if s:state['filter']
call s:update_items()
call s:notify_selection()
endif
endfunction
function! s:notify(name, data) abort
if has_key(s:state, 'on_event') | call s:state['on_event'](a:data, a:name) | endif
if has_key(s:state, 'on_' . a:name) | call s:state['on_' . a:name](a:data, a:name) | endif
endfunction
if exists('*win_execute')
function! s:win_execute(win_id, cmd) abort
call win_execute(a:win_id, a:cmd)
endfunction
else
function! s:win_execute(winid, cmd) abort
let l:original_winid = win_getid()
if l:original_winid == a:winid
exec a:cmd
else
if win_gotoid(a:winid)
exec a:cmd
call win_gotoid(l:original_winid)
end
endif
endfunction
endif
if exists('*trim')
function! s:trim(str) abort
return trim(a:str)
endfunction
else
function! s:trim(str) abort
return substitute(a:str, '^\s*\|\s*$', '', 'g')
endfunction
endif
" vim: set sw=2 ts=2 sts=2 et tw=78 foldmarker={{{,}}} foldmethod=marker spell:

View File

@@ -0,0 +1,70 @@
" https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress
let s:progress_ui = []
let s:enabled = 0
function! lsp#internal#work_done_progress#_enable() abort
if !g:lsp_work_done_progress_enabled | return | endif
if s:enabled | return | endif
let s:enabled = 1
let s:progress_ui = []
let s:Dispose = lsp#callbag#pipe(
\ lsp#stream(),
\ lsp#callbag#filter({x->has_key(x, 'response') && has_key(x['response'], 'method')
\ && x['response']['method'] ==# '$/progress' && has_key(x['response'], 'params')
\ && has_key(x['response']['params'], 'value') && type(x['response']['params']['value']) == type({})}),
\ lsp#callbag#subscribe({'next': {x->s:handle_work_done_progress(x['server'], x['response'])}})
\ )
endfunction
function! s:handle_work_done_progress(server, response) abort
let l:value = a:response['params']['value']
let l:token = a:response['params']['token']
let l:new = {
\ 'server': a:server,
\ 'token': l:token,
\ 'title': '',
\ 'message': '',
\ }
if l:value['kind'] ==# 'end'
let l:new['message'] = ''
let l:new['percentage'] = 100
call filter(s:progress_ui, {_, x->x['token'] !=# l:token || x['server'] !=# a:server})
elseif l:value['kind'] ==# 'begin'
let l:new['title'] = l:value['title']
call filter(s:progress_ui, {_, x->x['token'] !=# l:token || x['server'] !=# a:server})
call insert(s:progress_ui, l:new)
elseif l:value['kind'] ==# 'report'
let l:new['message'] = get(l:value, 'message', '')
if has_key(l:value, 'percentage')
" l:value['percentage'] is uinteger in specification.
" But some implementation return float. (e.g. clangd11)
" So we round it.
let l:new['percentage'] = float2nr(l:value['percentage'] + 0.5)
endif
let l:idx = match(s:progress_ui, l:token)
let l:new['title'] = s:progress_ui[l:idx]['title']
call filter(s:progress_ui, {_, x->x['token'] !=# l:token || x['server'] !=# a:server})
call insert(s:progress_ui, l:new)
endif
doautocmd <nomodeline> User lsp_progress_updated
endfunction
function! lsp#internal#work_done_progress#_disable() abort
if !s:enabled | return | endif
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
let s:enabled = 0
let s:progress_ui = []
endfunction
function! lsp#internal#work_done_progress#get_progress() abort
return s:progress_ui
endfunction

View File

@@ -0,0 +1,94 @@
" https://microsoft.github.io/language-server-protocol/specification#workspace_symbol
" options - {
" bufnr: bufnr('%') " optional
" server - 'server_name' " optional
" query: '' " optional
" }
function! lsp#internal#workspace_symbol#search#do(options) abort
if has_key(a:options, 'server')
let l:servers = [a:options['server']]
else
let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_symbol_provider(v:val)')
endif
if len(l:servers) == 0
echom 'textDocument/workspaceSymbol not supported'
call lsp#utils#error('textDocument/workspaceSymbol not supported')
return
endif
redraw | echo 'Retrieving workspace symbols ...'
let l:TextChangeSubject = lsp#callbag#makeSubject()
" use callbag debounce instead of quickpick debounce
call lsp#internal#ui#quickpick#open({
\ 'items': [],
\ 'input': get(a:options, 'query', ''),
\ 'key': 'text',
\ 'debounce': 0,
\ 'on_change': function('s:on_change', [l:TextChangeSubject]),
\ 'on_accept': function('s:on_accept'),
\ 'on_close': function('s:on_close'),
\ })
let s:Dispose = lsp#callbag#pipe(
\ l:TextChangeSubject,
\ lsp#callbag#debounceTime(250),
\ lsp#callbag#distinctUntilChanged(),
\ lsp#callbag#switchMap({query->
\ lsp#callbag#pipe(
\ lsp#callbag#fromList(l:servers),
\ lsp#callbag#tap({_->lsp#internal#ui#quickpick#busy(1)}),
\ lsp#callbag#flatMap({server->
\ lsp#callbag#pipe(
\ lsp#request(server, {
\ 'method': 'workspace/symbol',
\ 'params': {
\ 'query': query
\ }
\ }),
\ lsp#callbag#map({x->{'server': server, 'request': x['request'], 'response': x['response']}}),
\ )
\ }),
\ lsp#callbag#scan({acc, curr->add(acc, curr)}, []),
\ lsp#callbag#tap({x->s:update_ui_items(x)}),
\ lsp#callbag#tap({'complete': {->lsp#internal#ui#quickpick#busy(0)}}),
\ )
\ }),
\ lsp#callbag#subscribe({
\ 'error': {e->s:on_error(e)},
\ }),
\ )
" Notify empty query. Some servers may not return results when query is empty
call l:TextChangeSubject(1, '')
endfunction
function! s:on_change(TextChangeSubject, data, ...) abort
call a:TextChangeSubject(1, a:data['input'])
endfunction
function! s:update_ui_items(x) abort
let l:items = []
for l:i in a:x
let l:items += lsp#ui#vim#utils#symbols_to_loc_list(l:i['server'], l:i)
endfor
call lsp#internal#ui#quickpick#items(l:items)
endfunction
function! s:on_accept(data, name) abort
call lsp#internal#ui#quickpick#close()
call lsp#utils#location#_open_vim_list_item(a:data['items'][0], '')
endfunction
function! s:on_close(...) abort
if exists('s:Dispose')
call s:Dispose()
unlet s:Dispose
endif
endfunction
function! s:on_error(e) abort
call lsp#internal#ui#quickpick#close()
call lsp#log('LspWorkspaceSymbolSearch error', a:e)
endfunction