diff --git a/README.md b/README.md index 331a74c4..5ee2fbd5 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,9 @@ function! s:on_lsp_buffer_enabled() abort nmap [g (lsp-previous-diagnostic) nmap ]g (lsp-next-diagnostic) nmap K (lsp-hover) + + let g:lsp_format_sync_timeout = 1000 + autocmd! BufWritePre *.rs,*.go call execute('LspDocumentFormatSync') " refer to doc to add more commands endfunction diff --git a/autoload/lsp.vim b/autoload/lsp.vim index 2a2e97c0..d435caaf 100644 --- a/autoload/lsp.vim +++ b/autoload/lsp.vim @@ -915,7 +915,7 @@ endfunction " lsp#stream {{{ " -" example: +" example 1: " " function! s:on_textDocumentDiagnostics(x) abort " echom 'Diagnostics for ' . a:x['server'] . ' ' . json_encode(a:x['response']) @@ -927,8 +927,14 @@ endfunction " \ lsp#callbag#subscribe({ 'next':{x->s:on_textDocumentDiagnostics(x)} }), " \ ) " -function! lsp#stream() abort - return s:Stream +" example 2: +" call lsp#stream(1, { 'command': 'DocumentFormat' }) +function! lsp#stream(...) abort + if a:0 == 0 + return s:Stream + else + call s:Stream(a:1, a:2) + endif endfunction " }}} @@ -1102,6 +1108,7 @@ endfunction function! lsp#_new_command() abort let s:last_command_id += 1 + call s:Stream(1, { 'command': 1 }) return s:last_command_id endfunction diff --git a/autoload/lsp/internal/document_formatting.vim b/autoload/lsp/internal/document_formatting.vim new file mode 100644 index 00000000..05a0a871 --- /dev/null +++ b/autoload/lsp/internal/document_formatting.vim @@ -0,0 +1,85 @@ +" 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 ==# "\" + 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': getbufvar(a:options['bufnr'], '&tabstop'), + \ '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 + 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 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 diff --git a/autoload/lsp/internal/document_range_formatting.vim b/autoload/lsp/internal/document_range_formatting.vim new file mode 100644 index 00000000..64e8e83e --- /dev/null +++ b/autoload/lsp/internal/document_range_formatting.vim @@ -0,0 +1,124 @@ +" options - { +" bufnr: bufnr('%') " required +" type: '' " optional: defaults to visualmode(). overriden 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': getbufvar(a:options['bufnr'], '&tabstop'), + \ '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 + 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 diff --git a/autoload/lsp/ui/vim.vim b/autoload/lsp/ui/vim.vim index fd2786f9..5b089490 100644 --- a/autoload/lsp/ui/vim.vim +++ b/autoload/lsp/ui/vim.vim @@ -127,48 +127,6 @@ function! lsp#ui#vim#rename() abort call s:rename(l:server, input('new name: ', expand('')), lsp#get_position()) endfunction -function! s:document_format(sync) abort - let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_formatting_provider(v:val)') - let l:command_id = lsp#_new_command() - - if len(l:servers) == 0 - call s:not_supported('Document formatting') - return - endif - - " TODO: ask user to select server for formatting - let l:server = l:servers[0] - redraw | echo 'Formatting document ...' - call lsp#send_request(l:server, { - \ 'method': 'textDocument/formatting', - \ 'params': { - \ 'textDocument': lsp#get_text_document_identifier(), - \ 'options': { - \ 'tabSize': getbufvar(bufnr('%'), '&tabstop'), - \ 'insertSpaces': getbufvar(bufnr('%'), '&expandtab') ? v:true : v:false, - \ }, - \ }, - \ 'sync': a:sync, - \ 'on_notification': function('s:handle_text_edit', [l:server, l:command_id, 'document format']), - \ }) -endfunction - -function! lsp#ui#vim#document_format_sync() abort - let l:mode = mode() - if l:mode =~# '[vV]' || l:mode ==# "\" - return s:document_format_range(1) - endif - return s:document_format(1) -endfunction - -function! lsp#ui#vim#document_format() abort - let l:mode = mode() - if l:mode =~# '[vV]' || l:mode ==# "\" - return s:document_format_range(0) - endif - return s:document_format(0) -endfunction - function! lsp#ui#vim#stop_server(...) abort let l:name = get(a:000, 0, '') for l:server in lsp#get_allowed_servers() @@ -180,79 +138,6 @@ function! lsp#ui#vim#stop_server(...) abort endfor endfunction -function! s:get_selection_pos(type) abort - 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! s:document_format_range(sync, type) abort - let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_range_formatting_provider(v:val)') - let l:command_id = lsp#_new_command() - - if len(l:servers) == 0 - call s:not_supported('Document range formatting') - return - endif - - " TODO: ask user to select server for formatting - let l:server = l:servers[0] - - let [l:start_lnum, l:start_col, l:end_lnum, l:end_col] = s:get_selection_pos(a:type) - 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) - redraw | echo 'Formatting document range ...' - call lsp#send_request(l:server, { - \ 'method': 'textDocument/rangeFormatting', - \ 'params': { - \ 'textDocument': lsp#get_text_document_identifier(), - \ 'range': { - \ 'start': { 'line': l:start_lnum - 1, 'character': l:start_char }, - \ 'end': { 'line': l:end_lnum - 1, 'character': l:end_char }, - \ }, - \ 'options': { - \ 'tabSize': getbufvar(bufnr('%'), '&shiftwidth'), - \ 'insertSpaces': getbufvar(bufnr('%'), '&expandtab') ? v:true : v:false, - \ }, - \ }, - \ 'sync': a:sync, - \ 'on_notification': function('s:handle_text_edit', [l:server, l:command_id, 'range format']), - \ }) -endfunction - -function! lsp#ui#vim#document_range_format_sync() abort - return s:document_format_range(1, visualmode()) -endfunction - -function! lsp#ui#vim#document_range_format() abort - return s:document_format_range(0, visualmode()) -endfunction - -function! lsp#ui#vim#document_range_format_opfunc(type) abort - return s:document_format_range(1, a:type) -endfunction - function! lsp#ui#vim#workspace_symbol(query) abort let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_workspace_symbol_provider(v:val)') let l:command_id = lsp#_new_command() diff --git a/autoload/lsp/utils/args.vim b/autoload/lsp/utils/args.vim new file mode 100644 index 00000000..0ffb266e --- /dev/null +++ b/autoload/lsp/utils/args.vim @@ -0,0 +1,23 @@ +function! lsp#utils#args#_parse(args, opt) abort + let l:result = {} + for l:item in split(a:args, ' ') + let [l:key, l:value] = split(l:item, '=') + let l:key = l:key[2:] + if has_key(a:opt, l:key) + if has_key(a:opt[l:key], 'type') + let l:type = a:opt[l:key]['type'] + if l:type == type(v:true) + if l:value ==# 'false' || l:value ==# '0' || l:value ==# '' + let l:value = 0 + else + let l:value = 1 + endif + elseif l:type ==# type(0) + let l:value = str2nr(l:value) + endif + endif + endif + let l:result[l:key] = l:value + endfor + return l:result +endfunction diff --git a/doc/vim-lsp.txt b/doc/vim-lsp.txt index 896ddd8d..cd7be437 100644 --- a/doc/vim-lsp.txt +++ b/doc/vim-lsp.txt @@ -29,6 +29,7 @@ CONTENTS *vim-lsp-contents* g:lsp_diagnostics_echo_delay |g:lsp_diagnostics_echo_delay| g:lsp_diagnostics_float_cursor |g:lsp_diagnostics_float_cursor| g:lsp_diagnostics_float_delay |g:lsp_diagnostics_float_delay| + g:lsp_format_sync_timeout |g:lsp_format_sync_timeout| g:lsp_signs_enabled |g:lsp_signs_enabled| g:lsp_signs_priority |g:lsp_signs_priority| g:lsp_signs_priority_map |g:lsp_signs_priority_map| @@ -77,6 +78,7 @@ CONTENTS *vim-lsp-contents* LspDocumentFormat |:LspDocumentFormat| LspDocumentFormatSync |:LspDocumentFormatSync| LspDocumentRangeFormat |:LspDocumentRangeFormat| + LspDocumentRangeFormatSync |:LspDocumentRangeFormatSync| LspDocumentSymbol |:LspDocumentSymbol| LspHover |:LspHover| LspNextDiagnostic |:LspNextDiagnostic| @@ -161,6 +163,8 @@ http://downloads.sourceforge.net/luabinaries/lua-5.3.2_Win64_dllw4_lib.zip Set |g:lsp_semantic_enabled| to 0. +Set |g:lsp_format_sync_timeout| to a reasonable value such as `1000`. + ============================================================================== LANGUAGE SERVERS *vim-lsp-language-servers* @@ -471,6 +475,20 @@ g:lsp_diagnostics_float_delay *g:lsp_diagnostics_float_delay* let g:lsp_diagnostics_float_delay = 200 let g:lsp_diagnostics_float_delay = 1000 +g:lsp_format_sync_timeout *g:lsp_format_sync_timeout* + Type: |Number| + Default: `-1` + + Timeout milliseconds to abort `:LspDocumentFormatSync` or + `:LspDocumentRangeFormatSync`. Set to `-1` to disable timeout. Using + `BufWritePre` to execute sync commands may cause vim to hang when using + some language servers as starting the language server may be slow. Set the + timeout value to cancel sync format. + + Example: > + let g:lsp_format_sync_timeout = -1 + let g:lsp_format_sync_timeout = 1000 + g:lsp_signs_enabled *g:lsp_signs_enabled* Type: |Number| Default: `1` for vim/neovim with patch 8.1.0772 @@ -1215,7 +1233,8 @@ Format the entire document. LspDocumentFormatSync *:LspDocumentFormatSync* Same as |:LspDocumentFormat| but synchronous. Useful when running |:autocmd| -commands such as formatting before save. +commands such as formatting before save. Set |g:lsp_format_sync_timeout| to +configure timeouts. Example: > autocmd BufWritePre LspDocumentFormatSync @@ -1226,6 +1245,13 @@ LspDocumentRangeFormat *:LspDocumentRangeFormat* Format the current document selection. +LspDocumentRangeFormatSync *:LspDocumentRangeFormatSync* + +Same as |:LspDocumentRangeFormat| but synchronous. Useful when running running +:autocmd commands. Set |g:lsp_format_sync_timeout| to configure timeouts. + +Note that this may slow down vim. + LspDocumentSymbol *:LspDocumentSymbol* Gets the symbols for the current document. diff --git a/plugin/lsp.vim b/plugin/lsp.vim index 45034762..7d03eb04 100644 --- a/plugin/lsp.vim +++ b/plugin/lsp.vim @@ -9,6 +9,7 @@ let g:lsp_async_completion = get(g:, 'lsp_async_completion', 0) let g:lsp_log_file = get(g:, 'lsp_log_file', '') let g:lsp_log_verbose = get(g:, 'lsp_log_verbose', 1) let g:lsp_debug_servers = get(g:, 'lsp_debug_servers', []) +let g:lsp_format_sync_timeout = get(g:, 'lsp_format_sync_timeout', -1) let g:lsp_signs_enabled = get(g:, 'lsp_signs_enabled', exists('*sign_define') && (has('nvim') || has('patch-8.1.0772'))) let g:lsp_virtual_text_enabled = get(g:, 'lsp_virtual_text_enabled', exists('*nvim_buf_set_virtual_text')) let g:lsp_virtual_text_prefix = get(g:, 'lsp_virtual_text_prefix', '') @@ -97,10 +98,18 @@ command! LspTypeDefinition call lsp#ui#vim#type_definition(0, ) command! LspTypeHierarchy call lsp#internal#type_hierarchy#show() command! LspPeekTypeDefinition call lsp#ui#vim#type_definition(1) command! -nargs=? LspWorkspaceSymbol call lsp#ui#vim#workspace_symbol() -command! -range LspDocumentFormat call lsp#ui#vim#document_format() -command! -range LspDocumentFormatSync call lsp#ui#vim#document_format_sync() -command! -range LspDocumentRangeFormat call lsp#ui#vim#document_range_format() -command! -range LspDocumentRangeFormatSync call lsp#ui#vim#document_range_format_sync() +command! -range LspDocumentFormat call lsp#internal#document_formatting#format({ 'bufnr': bufnr('%') }) +command! -range -nargs=? LspDocumentFormatSync call lsp#internal#document_formatting#format( + \ extend({'bufnr': bufnr('%'), 'sync': 1 }, lsp#utils#args#_parse(, { + \ 'timeout': {'type': type(0)}, + \ 'sleep': {'type': type(0)}, + \ }))) +command! -range LspDocumentRangeFormat call lsp#internal#document_range_formatting#format({ 'bufnr': bufnr('%') }) +command! -range -nargs=? LspDocumentRangeFormatSync call lsp#internal#document_range_formatting#format( + \ extend({'bufnr': bufnr('%'), 'sync': 1 }, lsp#utils#args#_parse(, { + \ 'timeout': {'type': type(0)}, + \ 'sleep': {'type': type(0)}, + \ }))) command! LspImplementation call lsp#ui#vim#implementation(0, ) command! LspPeekImplementation call lsp#ui#vim#implementation(1) command! -nargs=0 LspStatus call lsp#print_server_status() @@ -141,10 +150,10 @@ nnoremap (lsp-type-definition) :call lsp#ui#vim#type_definition(0)(lsp-type-hierarchy) :call lsp#internal#type_hierarchy#show() nnoremap (lsp-peek-type-definition) :call lsp#ui#vim#type_definition(1) nnoremap (lsp-workspace-symbol) :call lsp#ui#vim#workspace_symbol('') -nnoremap (lsp-document-format) :call lsp#ui#vim#document_format() -vnoremap (lsp-document-format) :silent call lsp#ui#vim#document_range_format() -nnoremap (lsp-document-range-format) :set opfunc=lsp#ui#vim#document_range_format_opfuncg@ -xnoremap (lsp-document-range-format) :silent call lsp#ui#vim#document_range_format() +nnoremap (lsp-document-format) :call lsp#internal#document_formatting#format({ 'bufnr': bufnr('%') }) +vnoremap (lsp-document-format) :silent call lsp#internal#document_range_formatting#format({ 'bufnr': bufnr('%') }) +nnoremap (lsp-document-range-format) :set opfunc=lsp#internal#document_range_formatting#opfuncg@ +xnoremap (lsp-document-range-format) :silent call lsp#internal#document_range_formatting#format({ 'bufnr': bufnr('%') }) nnoremap (lsp-implementation) :call lsp#ui#vim#implementation(0) nnoremap (lsp-peek-implementation) :call lsp#ui#vim#implementation(1) nnoremap (lsp-status) :echo lsp#get_server_status()