"====================================================================== " " asynctasks.vim - " " Maintainer: skywind3000 (at) gmail.com, 2020 " " Last Modified: 2021/02/26 01:18 " Verision: 1.8.7 " " for more information, please visit: " https://github.com/skywind3000/asynctasks.vim " "====================================================================== " vim: set noet fenc=utf-8 ff=unix sts=4 sw=4 ts=4 : "---------------------------------------------------------------------- " internal variables "---------------------------------------------------------------------- let s:windows = has('win32') || has('win64') || has('win16') || has('win95') let s:scriptname = expand(':p') let s:scripthome = fnamemodify(s:scriptname, ':h:h') "---------------------------------------------------------------------- " default values "---------------------------------------------------------------------- " system if !exists('g:asynctasks_system') let g:asynctasks_system = (s:windows == 0)? 'linux' : 'win32' endif " task profile let g:asynctasks_profile = get(g:, 'asynctasks_profile', 'debug') " local config, can be a comma separated list like '.tasks,.git/.tasks' let g:asynctasks_config_name = get(g:, 'asynctasks_config_name', '.tasks') " global config in every runtimepath let g:asynctasks_rtp_config = get(g:, 'asynctasks_rtp_config', 'tasks.ini') " additional global configs let g:asynctasks_extra_config = get(g:, 'asynctasks_extra_config', []) " config by vimrc let g:asynctasks_tasks = get(g:, 'asynctasks_tasks', {}) " task environment variables let g:asynctasks_environ = get(g:, 'asynctasks_environ', {}) " features let g:asynctasks_feature = get(g:, 'asynctasks_feature', {}) " confirm file name in :AsyncEdit ? let g:asynctasks_confirm = get(g:, 'asynctasks_confirm', 1) " terminal mode: tab/curwin/top/bottom/left/right/quickfix/external let g:asynctasks_term_pos = get(g:, 'asynctasks_term_pos', 'quickfix') " width of vertical terminal split let g:asynctasks_term_cols = get(g:, 'asynctasks_term_cols', '') " height of horizontal terminal split let g:asynctasks_term_rows = get(g:, 'asynctasks_term_rows', '') " set to zero to keep focus when open a terminal in a split let g:asynctasks_term_focus = get(g:, 'asynctasks_term_focus', 1) " make internal terminal tab reusable let g:asynctasks_term_reuse = get(g:, 'asynctasks_term_reuse', 0) " whether set bufhidden to 'hide' in terminal window let g:asynctasks_term_hidden = get(g:, 'asynctasks_term_hidden', 0) " set nolisted to terminal buffer ? let g:asynctasks_term_listed = get(g:, 'asynctasks_term_listed', 1) " set to 1 to pass arguments in a safe way (intermediate script) let g:asynctasks_term_safe = get(g:, 'asynctasks_term_safe', 0) " strict to detect $(VIM_CWORD) to avoid empty string let g:asynctasks_strict = get(g:, 'asynctasks_strict', 1) " notify when finished (output=quickfix), can be: '', 'echo', 'bell' let g:asynctasks_notify = get(g:, 'asynctasks_notify', '') " set to zero to create .tasks without template let g:asynctasks_template = get(g:, 'asynctasks_template', 1) " set to 1 to remember last user input for each variable let g:asynctasks_remember = get(g:, 'asynctasks_remember', 0) " last user input, key is 'taskname:variable' let g:asynctasks_history = get(g:, 'asynctasks_history', {}) " control how to open a split window in AsyncTaskEdit let g:asynctasks_edit_split = get(g:, 'asynctasks_edit_split', '') " Add highlight colors if they don't exist. if !hlexists('AsyncRunSuccess') highlight link AsyncRunSuccess ModeMsg endif if !hlexists('AsyncRunFailure') highlight link AsyncRunFailure ErrorMsg endif "---------------------------------------------------------------------- " tuning "---------------------------------------------------------------------- " increase asyncrun speed if exists('g:asyncrun_timer') == 0 let g:asyncrun_timer = 100 elseif g:asyncrun_timer < 100 let g:asyncrun_timer = 100 endif " disable autocmd for each update let g:asyncrun_skip = 1 "---------------------------------------------------------------------- " internal object "---------------------------------------------------------------------- let s:private = { 'cache':{}, 'rtp':{}, 'local':{}, 'tasks':{} } let s:error = '' let s:index = 0 "---------------------------------------------------------------------- " internal function "---------------------------------------------------------------------- " display in cmdline function! s:errmsg(msg) redraw | echo '' | redraw echohl ErrorMsg echom 'Error: ' . a:msg echohl NONE let s:index += 1 endfunc " display in cmdline function! s:warning(msg) redraw | echo '' | redraw echohl WarningMsg echom 'Warning: ' . a:msg echohl NONE let s:index += 1 endfunc " trim leading & trailing spaces function! s:strip(text) return substitute(a:text, '^\s*\(.\{-}\)\s*$', '\1', '') endfunc " partition function! s:partition(text, sep) let pos = stridx(a:text, a:sep) if pos < 0 return [a:text, '', ''] else let size = strlen(a:sep) let head = strpart(a:text, 0, pos) let sep = strpart(a:text, pos, size) let tail = strpart(a:text, pos + size) return [head, sep, tail] endif endfunc " replace string function! s:replace(text, old, new) let l:data = split(a:text, a:old, 1) return join(l:data, a:new) endfunc " load ini file function! s:readini(source) if type(a:source) == type('') if !filereadable(a:source) return -1 endif let content = readfile(a:source) elseif type(a:source) == type([]) let content = a:source else return -2 endif let sections = {} let current = 'default' let index = 0 for line in content let t = substitute(line, '^\s*\(.\{-}\)\s*$', '\1', '') let index += 1 if t == '' continue elseif t =~ '^[;#].*$' continue elseif t =~ '^\[.*\]$' let current = substitute(t, '^\[\s*\(.\{-}\)\s*\]$', '\1', '') if !has_key(sections, current) let sections[current] = {} endif else let pos = stridx(t, '=') if pos >= 0 let key = strpart(t, 0, pos) let val = strpart(t, pos + 1) let key = substitute(key, '^\s*\(.\{-}\)\s*$', '\1', '') let val = substitute(val, '^\s*\(.\{-}\)\s*$', '\1', '') if !has_key(sections, current) let sections[current] = {} endif let sections[current][key] = val endif endif endfor return sections endfunc " returns nearest parent directory contains one of the markers function! s:find_root(name, markers, strict) let name = fnamemodify((a:name != '')? a:name : bufname('%'), ':p') let finding = '' " iterate all markers for marker in a:markers if marker != '' " search as a file let x = findfile(marker, name . '/;') let x = (x == '')? '' : fnamemodify(x, ':p:h') " search as a directory let y = finddir(marker, name . '/;') let y = (y == '')? '' : fnamemodify(y, ':p:h:h') " which one is the nearest directory ? let z = (strchars(x) > strchars(y))? x : y " keep the nearest one in finding let finding = (strchars(z) > strchars(finding))? z : finding endif endfor if finding == '' let path = (a:strict == 0)? fnamemodify(name, ':h') : '' else let path = fnamemodify(finding, ':p') endif if has('win32') || has('win16') || has('win64') || has('win95') let path = substitute(path, '\/', '\', 'g') endif if path =~ '[\/\\]$' let path = fnamemodify(path, ':h') endif return path endfunc " find project root function! s:project_root(name, strict) let markers = ['.project', '.git', '.hg', '.svn', '.root'] if exists('g:asyncrun_rootmarks') let markers = g:asyncrun_rootmarks endif return s:find_root(a:name, markers, a:strict) endfunc " change directory in a proper way function! s:chdir(path) if has('nvim') let cmd = haslocaldir()? 'lcd' : (haslocaldir(-1, 0)? 'tcd' : 'cd') else let cmd = haslocaldir()? ((haslocaldir() == 1)? 'lcd' : 'tcd') : 'cd' endif silent execute cmd . ' '. fnameescape(a:path) endfunc " join two path function! s:path_join(home, name) let l:size = strlen(a:home) if l:size == 0 | return a:name | endif let l:last = strpart(a:home, l:size - 1, 1) if has("win32") || has("win64") || has("win16") || has('win95') let l:first = strpart(a:name, 0, 1) if l:first == "/" || l:first == "\\" let head = strpart(a:home, 1, 2) if index([":\\", ":/"], head) >= 0 return strpart(a:home, 0, 2) . a:name endif return a:name elseif index([":\\", ":/"], strpart(a:name, 1, 2)) >= 0 return a:name endif if l:last == "/" || l:last == "\\" return a:home . a:name else return a:home . '/' . a:name endif else if strpart(a:name, 0, 1) == "/" return a:name endif if l:last == "/" return a:home . a:name else return a:home . '/' . a:name endif endif endfunc " get absolute path function! s:abspath(path) let f = a:path if f =~ "'." try redir => m silent exe ':marks' f[1] redir END let f = split(split(m, '\n')[-1])[-1] let f = filereadable(f)? f : '' catch let f = '%' endtry endif if f == '%' let f = expand('%') if &bt == 'terminal' || &bt == 'nofile' let f = '' endif elseif f =~ '^\~[\/\\]' let f = expand(f) endif let f = fnamemodify(f, ':p') if s:windows != 0 let f = substitute(f, '\/', '\\', 'g') else let f = substitute(f, '\\', '\/', 'g') endif let f = substitute(f, '\\', '\/', 'g') if f =~ '\/$' let f = fnamemodify(f, ':h') endif return f endfunc " config names function! s:config_names() let cname = g:asynctasks_config_name let parts = (type(cname) == 1)? split(cname, ',') : cname let names = [] for name in parts let t = s:strip(name) if t != '' let names += [t] endif endfor return names endfunc " search files upwards function! s:search_parent(path) let config = s:config_names() let output = [] let root = s:abspath(a:path) if len(config) == 0 return [] endif while 1 for name in config let test = s:path_join(root, name) let test = s:abspath(test) if filereadable(test) let output += [test] endif endfor let prev = root let root = fnamemodify(root, ':h') if root == prev break endif endwhile call reverse(output) return output endfunc " extract: [cmd, options] function! s:ExtractOpt(command) let cmd = a:command let opts = {} while cmd =~# '^-\%(\w\+\)\%([= ]\|$\)' let opt = matchstr(cmd, '^-\zs\w\+') if cmd =~ '^-\w\+=' let val = matchstr(cmd, '^-\w\+=\zs\%(\\.\|\S\)*') else let val = (opt == 'cwd')? '' : 1 endif let opts[opt] = substitute(val, '\\\(\s\)', '\1', 'g') let cmd = substitute(cmd, '^-\w\+\%(=\%(\\.\|\S\)*\)\=\s*', '', '') endwhile return [cmd, opts] endfunc " change case for comparation function! s:pathcase(path) if s:windows == 0 return (has('win32unix') == 0)? (a:path) : tolower(a:path) else return tolower(tr(a:path, '/', '\')) endif endfunc "---------------------------------------------------------------------- " read ini in cache "---------------------------------------------------------------------- function! s:cache_load_ini(name) let name = (stridx(a:name, '~') >= 0)? expand(a:name) : a:name let name = s:abspath(name) let p1 = name if s:windows || has('win32unix') let p1 = tr(tolower(p1), "\\", '/') endif let ts = getftime(name) if ts < 0 let s:error = 'cannot load ' . a:name return -1 endif if has_key(s:private.cache, p1) let obj = s:private.cache[p1] if ts <= obj.ts return obj endif endif let config = s:readini(name) if type(config) != v:t_dict let s:error = 'syntax error in '. a:name . ' line '. config return config endif let s:private.cache[p1] = {} let obj = s:private.cache[p1] let obj.ts = ts let obj.name = name let obj.config = config let obj.keys = keys(config) let ininame = name let inihome = fnamemodify(name, ':h') for sect in obj.keys let section = obj.config[sect] for key in keys(section) let val = section[key] let val = s:replace(val, '$(VIM_INIHOME)', inihome) let val = s:replace(val, '$(VIM_INIFILE)', ininame) let section[key] = val endfor endfor return obj endfunc "---------------------------------------------------------------------- " check requirement "---------------------------------------------------------------------- function! s:requirement(what) if a:what == 'asyncrun' if exists(':AsyncRun') == 0 let t = 'asyncrun is required, install from ' call s:errmsg(t . '"skywind3000/asyncrun.vim"') return 0 endif silent! exec "AsyncRun -mode=load" if exists('*asyncrun#version') == 0 let t = 'asyncrun is not loaded correctly ' call s:errmsg(t . 'try to avoid lazy load on asyncrun') return 0 endif let target = '2.4.3' if s:version_compare(asyncrun#version(), target) < 0 let t = 'asyncrun ' . target . ' or above is required, ' call s:errmsg(t . 'update from "skywind3000/asyncrun.vim"') return 0 endif endif return 1 endfunc "---------------------------------------------------------------------- " split 'text:colon/slash' into: [text, colon, slash] "---------------------------------------------------------------------- function! s:trinity_split(text) let text = a:text let p1 = stridx(text, ':') let p2 = stridx(text, '/') if p1 < 0 && p2 < 0 return [text, '', ''] endif let parts = split(text, '[:/]') if p1 >= 0 && p2 >= 0 if p1 < p2 return [parts[0], parts[1], parts[2]] else return [parts[0], parts[2], parts[1]] endif elseif p1 >= 0 && p2 < 0 return [parts[0], parts[1], ''] elseif p1 < 0 && p2 >= 0 return [parts[0], '', parts[1]] endif endfunc "---------------------------------------------------------------------- " merge two tasks "---------------------------------------------------------------------- function! s:config_merge(target, source, ininame, mode) let special = [] for key in keys(a:source) if stridx(key, ':') >= 0 let special += [key] elseif stridx(key, '/') >= 0 let special += [key] elseif key != '*' let a:target[key] = a:source[key] if a:ininame != '' let a:target[key].__name__ = a:ininame endif if a:mode != '' let a:target[key].__mode__ = a:mode endif else endif endfor for key in special let parts = s:trinity_split(key) let name = s:strip(parts[0]) let parts[1] = s:strip(parts[1]) let parts[2] = s:strip(parts[2]) if parts[1] != '' let profile = parts[1] if profile != g:asynctasks_profile continue endif endif if parts[2] != '' let feature = get(g:asynctasks_feature, parts[2], 0) if feature == 0 continue endif endif let a:target[name] = a:source[key] if a:ininame != '' let a:target[name].__name__ = a:ininame endif if a:mode != '' let a:target[name].__mode__ = a:mode endif endfor return a:target endfunc "---------------------------------------------------------------------- " collect config in rtp "---------------------------------------------------------------------- function! s:collect_rtp_config() abort let names = [] if g:asynctasks_rtp_config != '' let rtp_name = g:asynctasks_rtp_config for rtp in split(&rtp, ',') if rtp != '' let path = s:abspath(rtp . '/' . rtp_name) if filereadable(path) let names += [path] endif endif endfor let t = s:abspath(expand('~/.vim/' . rtp_name)) if filereadable(t) let names += [t] endif if $XDG_CONFIG_HOME != '' let t = $XDG_CONFIG_HOME . '/nvim/' . rtp_name else let t = expand('~/.config/nvim') . '/' . rtp_name endif if filereadable(t) let names += [t] endif endif for name in g:asynctasks_extra_config let name = s:abspath(name) if filereadable(name) let names += [name] endif endfor let newname = [] let checker = {} call reverse(names) for name in names let key = name if s:windows || has('win32unix') let key = fnamemodify(key, ':p') let key = tr(tolower(key), "\\", '/') endif if has_key(checker, key) == 0 let newname += [tr(name, "\\", '/')] let checker[key] = 1 endif endfor call reverse(newname) let names = newname let s:private.rtp.ini = {} let config = {} let s:error = '' for name in names let obj = s:cache_load_ini(name) if s:error == '' let mode = 'global' call s:config_merge(s:private.rtp.ini, obj.config, name, mode) else call s:errmsg(s:error) let s:error = '' endif endfor let config = deepcopy(s:private.rtp.ini) call s:config_merge(config, g:asynctasks_tasks, '