use callbag to implement document format with timeout (#956)

* use callbag to implement document format with timeout

* document lsp#stream() with example

* send message to stream when using lsp#_new_command

* use -- for args parsing and add g:lsp_document_format_sync_timeout

* throw string exception

* use new func for <plug>(lsp-document-format)

* document g:lsp_format_sync_timeout

* rename to document_formatting

* add document_range_formatting.vim

* remove formatting from ui/vim.vim

* document :LspDocumentRangeFormatSync

* add LspDocumentFormatSync example in README.md
This commit is contained in:
Prabir Shrestha
2020-12-22 22:08:32 -08:00
committed by GitHub
parent 9dce8c50ed
commit b316729ef5
8 changed files with 289 additions and 127 deletions
+3
View File
@@ -37,6 +37,9 @@ function! s:on_lsp_buffer_enabled() abort
nmap <buffer> [g <Plug>(lsp-previous-diagnostic)
nmap <buffer> ]g <Plug>(lsp-next-diagnostic)
nmap <buffer> K <plug>(lsp-hover)
let g:lsp_format_sync_timeout = 1000
autocmd! BufWritePre *.rs,*.go call execute('LspDocumentFormatSync')
" refer to doc to add more commands
endfunction
+10 -3
View File
@@ -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
@@ -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 ==# "\<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': 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
@@ -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
-115
View File
@@ -127,48 +127,6 @@ function! lsp#ui#vim#rename() abort
call s:rename(l:server, input('new name: ', expand('<cword>')), 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 ==# "\<C-V>"
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 ==# "\<C-V>"
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()
+23
View File
@@ -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
+27 -1
View File
@@ -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 <buffer> 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.
+17 -8
View File
@@ -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, <q-mods>)
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(<q-args>)
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(<q-args>, {
\ '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(<q-args>, {
\ 'timeout': {'type': type(0)},
\ 'sleep': {'type': type(0)},
\ })))
command! LspImplementation call lsp#ui#vim#implementation(0, <q-mods>)
command! LspPeekImplementation call lsp#ui#vim#implementation(1)
command! -nargs=0 LspStatus call lsp#print_server_status()
@@ -141,10 +150,10 @@ nnoremap <plug>(lsp-type-definition) :<c-u>call lsp#ui#vim#type_definition(0)<cr
nnoremap <plug>(lsp-type-hierarchy) :<c-u>call lsp#internal#type_hierarchy#show()<cr>
nnoremap <plug>(lsp-peek-type-definition) :<c-u>call lsp#ui#vim#type_definition(1)<cr>
nnoremap <plug>(lsp-workspace-symbol) :<c-u>call lsp#ui#vim#workspace_symbol('')<cr>
nnoremap <plug>(lsp-document-format) :<c-u>call lsp#ui#vim#document_format()<cr>
vnoremap <plug>(lsp-document-format) :<Home>silent <End>call lsp#ui#vim#document_range_format()<cr>
nnoremap <plug>(lsp-document-range-format) :<c-u>set opfunc=lsp#ui#vim#document_range_format_opfunc<cr>g@
xnoremap <plug>(lsp-document-range-format) :<Home>silent <End>call lsp#ui#vim#document_range_format()<cr>
nnoremap <plug>(lsp-document-format) :<c-u>call lsp#internal#document_formatting#format({ 'bufnr': bufnr('%') })<cr>
vnoremap <plug>(lsp-document-format) :<Home>silent <End>call lsp#internal#document_range_formatting#format({ 'bufnr': bufnr('%') })<cr>
nnoremap <plug>(lsp-document-range-format) :<c-u>set opfunc=lsp#internal#document_range_formatting#opfunc<cr>g@
xnoremap <plug>(lsp-document-range-format) :<Home>silent <End>call lsp#internal#document_range_formatting#format({ 'bufnr': bufnr('%') })<cr>
nnoremap <plug>(lsp-implementation) :<c-u>call lsp#ui#vim#implementation(0)<cr>
nnoremap <plug>(lsp-peek-implementation) :<c-u>call lsp#ui#vim#implementation(1)<cr>
nnoremap <plug>(lsp-status) :<c-u>echo lsp#get_server_status()<cr>