1
0
Fork 0
mirror of synced 2024-06-01 15:01:10 -04:00
ultimate-vim/sources_non_forked/vim-misc/autoload/xolox/misc/async.vim
2017-11-27 13:43:13 +08:00

262 lines
13 KiB
VimL
Executable file

" Asynchronous Vim script evaluation.
"
" Author: Peter Odding <peter@peterodding.com>
" Last Change: September 17, 2014
" URL: http://peterodding.com/code/vim/misc/
"
" The `xolox#misc#async#call()` function builds on top of `xolox#misc#os#exec()`
" to support asynchronous evaluation of Vim scripts. The first (and for now
" only) use case is my [vim-easytags][] plug-in which has a bunch of
" conflicting requirements:
"
" 1. I want the [vim-easytags][] plug-in to be as portable as possible.
" Ideally everything is implemented in Vim script because that's the only
" thing I can rely on to be available for all potential users of the
" plug-in!
"
" 2. Because of point one I've been forced to implement tags file reading,
" parsing, (fold case) sorting and writing in Vim script. This is fine for
" small tags files but once they grow to a couple of megabytes it becomes
" annoying because Vim is unresponsive during tags file updates (key
" presses are fortunately buffered due to Vim's input model but that
" doesn't make it a nice user experience :-).
"
" 3. I could (and did in the past) come up with all sorts of hacks to speed
" things up without switching away from Vim script, but none of them are
" going to solve the fundamental problem that Vim's unresponsive hiccups
" become longer as tags files grow larger.
"
" By now it should be clear where this is heading: _Why not handle tags file
" updates in a Vim process that runs in the background without blocking the
" Vim process that the user is interacting with?_ It turns out that there are
" quite a few details to take care of, but with those out of the way, it might
" just work! I'm actually hoping to make asynchronous updates the default mode
" in [vim-easytags][]. This means I need this functionality to be as
" portable and robust as possible.
"
" **Status:** This code has seen little testing so I wouldn't trust it too
" much just yet. On the other hand, as I said, my intention is to make this
" functionality as portable and robust as possible. You be the judge :-).
"
" [vim-easytags]: http://peterodding.com/code/vim/easytags/
if !exists('g:xolox#misc#async#counter')
" Increasing integer number used to match asynchronous responses to the
" requests that generated them.
let g:xolox#misc#async#counter = 1
endif
if !exists('g:xolox#misc#async#requests')
" Queue of asynchronous requests that haven't received a response yet.
let g:xolox#misc#async#requests = {}
endif
function! xolox#misc#async#call(options) " {{{1
" Call a Vim script function asynchronously by starting a hidden Vim process
" in the background. Once the function returns the hidden Vim process
" terminates itself. This function takes a single argument which is a
" dictionary with the following key/value pairs:
"
" - **function** (required): The name of the Vim function to call inside
" the child process (a string). I suggest using an [autoload][] function
" for this, see below.
"
" - **arguments** (optional): A list of arguments to pass to the function.
" This list is serialized to a string using [string()][] and deserialized
" using [eval()][].
"
" - **callback** (optional): The name of a Vim function to call in the
" parent process when the child process has completed (a string).
"
" - **clientserver** (optional): If this is true (1) the child process will
" notify the parent process when it has finished (the default is true).
" This works using Vim's client/server support which is not always
" available. As a fall back Vim's [CursorHold][] automatic command is
" also supported (although the effect is not quite as instantaneous :-).
"
" This functionality is experimental and non trivial to use, so consider
" yourself warned :-).
"
" **Limitations**
"
" I'm making this functionality available in [vim-misc][] because I think it
" can be useful to other plug-ins, however if you are going to use it you
" should be aware of the following limitations:
"
" - Because of the use of multiple processes this functionality is only
" suitable for 'heavy' tasks.
"
" - The function arguments are serialized to a string which is passed to
" the hidden Vim process as a command line argument, so the amount of
" data you can pass will be limited by your operating environment.
"
" - The hidden Vim process is explicitly isolated from the user in several
" ways (see below for more details). This is to make sure that the hidden
" Vim processes are fast and don't clobber the user's editing sessions in
" any way.
"
" **Changes to how Vim normally works**
"
" You have to be aware that the hidden Vim process is initialized in a
" specific way that is very different from your regular Vim editing
" sessions:
"
" - Your [vimrc][] file is ignored using the `-u NONE` command line option.
"
" - Your [gvimrc][] file (if you even knew it existed ;-) is ignored using
" the `-U NONE` command line option.
"
" - Plug-in loading is skipped using the `--noplugin` command line option.
"
" - Swap files (see [swap-file][]) are disabled using the `-n` command line
" option. This makes sure asynchronous Vim processes don't disturb the
" user's editing session.
"
" - Your [viminfo][] file is ignored using the `-i NONE` command line
" option. Just like with swap files this makes sure asynchronous Vim
" processes don't disturb the user's editing session.
"
" - No-compatible mode is enabled using the `-N` command line option
" (usually the existence of your vimrc script would have achieved the
" same effect but since we disable loading of your vimrc we need to spell
" things out for Vim).
"
" **Use an auto-load function**
"
" The function you want to call is identified by its name which has to be
" defined, but I just explained above that all regular initialization is
" disabled for asynchronous Vim processes, so what gives? The answer is to
" use an [autoload][] function. This should work fine because the
" asynchronous Vim process 'inherits' the value of the ['runtimepath'][]
" option from your editing session.
"
" ['runtimepath']: http://vimdoc.sourceforge.net/htmldoc/options.html#'runtimepath'
" [autoload]: http://vimdoc.sourceforge.net/htmldoc/eval.html#autoload
" [CursorHold]: http://vimdoc.sourceforge.net/htmldoc/autocmd.html#CursorHold
" [eval()]: http://vimdoc.sourceforge.net/htmldoc/eval.html#eval()
" [gvimrc]: http://vimdoc.sourceforge.net/htmldoc/gui.html#gvimrc
" [string()]: http://vimdoc.sourceforge.net/htmldoc/eval.html#string()
" [swap-file]: http://vimdoc.sourceforge.net/htmldoc/recover.html#swap-file
" [vim-misc]: http://peterodding.com/code/vim/misc/
" [viminfo]: http://vimdoc.sourceforge.net/htmldoc/starting.html#viminfo
" [vimrc]: http://vimdoc.sourceforge.net/htmldoc/starting.html#vimrc
let unique_number = g:xolox#misc#async#counter
let g:xolox#misc#async#counter += 1
let request = {'function': a:options['function']}
let request['arguments'] = get(a:options, 'arguments', [])
let request['starttime'] = xolox#misc#timer#start()
let request['number'] = unique_number
let callback = get(a:options, 'callback')
if !empty(callback)
let request['callback'] = callback
endif
if get(a:options, 'clientserver', 1) && !empty(v:servername)
let request['servername'] = v:servername
else
let temporary_file = tempname()
let request['temporary_file'] = temporary_file
endif
let vim_command = printf('let &rtp = %s | call xolox#misc#async#inside_child(%s)', string(&rtp), string(request))
call xolox#misc#msg#debug("vim-misc %s: Generated asynchronous Vim command #%i: %s", g:xolox#misc#version, unique_number, vim_command)
let quoted_program = xolox#misc#escape#shell(xolox#misc#os#find_vim('vim'))
let quoted_command = xolox#misc#escape#shell(vim_command)
let shell_command = printf('%s -u NONE -U NONE --noplugin -n -N -i NONE --cmd %s', quoted_program, quoted_command)
call xolox#misc#msg#debug("vim-misc %s: Generated asynchronous shell command #%i: %s", g:xolox#misc#version, unique_number, shell_command)
call xolox#misc#os#exec({'command': shell_command, 'async': 1})
let g:xolox#misc#async#requests[unique_number] = request
endfunction
function! xolox#misc#async#inside_child(request) " {{{1
" Entry point inside the hidden Vim process that runs in the background.
" Invoked indirectly by `xolox#misc#async#call()` because it runs a command
" similar to the following:
"
" vim --cmd 'call xolox#misc#async#inside_child(...)'
"
" This function is responsible for calling the user defined function,
" capturing exceptions and reporting the results back to the parent Vim
" process using Vim's client/server support or a temporary file.
try
let response = {'number': a:request['number']}
let starttime = xolox#misc#timer#start()
try
" Call the user defined function and store its result.
let response['result'] = call(a:request['function'], a:request['arguments'])
catch
" Intercept errors raised by the user defined function.
let response['exception'] = v:exception
let response['throwpoint'] = v:throwpoint
endtry
" Record the elapsed time.
let response['elapsed_time'] = xolox#misc#timer#convert(starttime)
" Communicate the results back to the master Vim process.
let servername = get(a:request, 'servername', '')
if !empty(servername)
" Actively notify the parent process using Vim's client/server support?
call remote_expr(servername, printf('xolox#misc#async#callback_to_parent(%s)', string(response)))
else
" 'Passively' notify the parent process by creating the expected
" temporary file.
call xolox#misc#persist#save(a:request['temporary_file'], response)
endif
finally
" Make sure we terminate this hidden Vim process.
quitall!
endtry
endfunction
function! xolox#misc#async#callback_to_parent(response) " {{{1
" When Vim was compiled with client/server support this function (in the
" parent process) will be called by `xolox#misc#async#inside_child()` (in
" the child process) after the user defined function has returned. This
" enables more or less instant callbacks after running an asynchronous
" function.
let unique_number = a:response['number']
let request = g:xolox#misc#async#requests[unique_number]
call xolox#misc#timer#stop("vim-misc %s: Processing asynchronous callback #%i after %s ..", g:xolox#misc#version, unique_number, request['starttime'])
call remove(g:xolox#misc#async#requests, unique_number)
let callback = get(request, 'callback')
if !empty(callback)
call call(callback, [a:response])
endif
endfunction
function! xolox#misc#async#periodic_callback() " {{{1
" When client/server support is not being used the vim-misc plug-in
" improvises: It uses Vim's [CursorHold][] event to periodically check if an
" asynchronous process has written its results to one of the expected
" temporary files. If a response is found the temporary file is read and
" deleted and then `xolox#misc#async#callback_to_parent()` is called to
" process the response.
"
" [CursorHold]: http://vimdoc.sourceforge.net/htmldoc/autocmd.html#CursorHold
if !empty(g:xolox#misc#async#requests)
let num_processed = 0
call xolox#misc#msg#debug("vim-misc %s: Checking for asynchronous responses (%i responses not yet received) ..", g:xolox#misc#version, len(g:xolox#misc#async#requests))
for unique_number in sort(keys(g:xolox#misc#async#requests))
let request = g:xolox#misc#async#requests[unique_number]
let temporary_file = get(request, 'temporary_file', '')
if !empty(temporary_file) && getfsize(temporary_file) > 0
try
call xolox#misc#msg#debug("vim-misc %s: Found asynchronous response by %s in %s ..", g:xolox#misc#version, request['function'], temporary_file)
call xolox#misc#async#callback_to_parent(xolox#misc#persist#load(temporary_file))
let num_processed += 1
finally
call delete(temporary_file)
endtry
endif
endfor
call xolox#misc#msg#debug("vim-misc %s: Processed %i asynchronous responses (%i responses not yet received).", g:xolox#misc#version, num_processed, len(g:xolox#misc#async#requests))
endif
endfunction
" }}}1
" The interval in the options below is set to one (1) although the default
" value for &updatetime is four seconds. Because vim-misc never modifies
" &updatetime the interval will effectively default to four seconds unless the
" user has set &updatetime to a lower value themselves.
call xolox#misc#cursorhold#register({'function': 'xolox#misc#async#periodic_callback', 'interval': 1})
" vim: ts=2 sw=2 et