mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-03-25 19:32:20 +08:00
535 lines
18 KiB
VimL
535 lines
18 KiB
VimL
"=============================================================================
|
|
" runner.vim --- code runner for SpaceVim
|
|
" Copyright (c) 2016-2022 Wang Shidong & Contributors
|
|
" Author: Shidong Wang < wsdjeg@outlook.com >
|
|
" URL: https://spacevim.org
|
|
" License: GPLv3
|
|
"=============================================================================
|
|
|
|
""
|
|
" @section runner, plugins-runner
|
|
" @parentsection plugins
|
|
" The `code runner` plugin provides the ability to run code snippet or code
|
|
" file for a variety of programming languages, as well as running custom commands.
|
|
"
|
|
" @subsection Key bindings
|
|
" >
|
|
" Key binding Description
|
|
" SPC s r start default code runner
|
|
" q close coder runner window
|
|
" i insert text to background process
|
|
" <
|
|
|
|
let s:runners = {}
|
|
|
|
let s:JOB = SpaceVim#api#import('job')
|
|
let s:BUFFER = SpaceVim#api#import('vim#buffer')
|
|
let s:STRING = SpaceVim#api#import('data#string')
|
|
let s:FILE = SpaceVim#api#import('file')
|
|
let s:VIM = SpaceVim#api#import('vim')
|
|
let s:SYS = SpaceVim#api#import('system')
|
|
let s:ICONV = SpaceVim#api#import('iconv')
|
|
|
|
let s:LOGGER =SpaceVim#logger#derive('runner')
|
|
|
|
" use code runner buffer for tab
|
|
"
|
|
"
|
|
|
|
" the buffer number of code runner
|
|
let s:code_runner_bufnr = 0
|
|
" @fixme win_getid requires vim 7.4.1557
|
|
let s:winid = -1
|
|
let s:target = ''
|
|
let s:runner_lines = 0
|
|
let s:runner_jobid = 0
|
|
let s:runner_status = {
|
|
\ 'is_running' : 0,
|
|
\ 'has_errors' : 0,
|
|
\ 'exit_code' : 0
|
|
\ }
|
|
|
|
|
|
let s:task_status = {}
|
|
let s:task_stdout = {}
|
|
let s:task_stderr = {}
|
|
let s:task_problem_matcher = {}
|
|
|
|
function! s:open_win() abort
|
|
if s:code_runner_bufnr !=# 0 && bufexists(s:code_runner_bufnr) && index(tabpagebuflist(), s:code_runner_bufnr) !=# -1
|
|
return
|
|
endif
|
|
botright split __runner__
|
|
let lines = &lines * 30 / 100
|
|
exe 'resize ' . lines
|
|
setlocal buftype=nofile bufhidden=wipe nobuflisted nolist noswapfile nowrap cursorline nospell nonu norelativenumber winfixheight nomodifiable
|
|
set filetype=SpaceVimRunner
|
|
nnoremap <silent><buffer> q :call <SID>close()<cr>
|
|
nnoremap <silent><buffer> i :call <SID>insert()<cr>
|
|
nnoremap <silent><buffer> <C-c> :call <SID>stop_runner()<cr>
|
|
augroup spacevim_runner
|
|
autocmd!
|
|
autocmd BufWipeout <buffer> call <SID>stop_runner()
|
|
augroup END
|
|
let s:code_runner_bufnr = bufnr('%')
|
|
if exists('*win_getid')
|
|
let s:winid = win_getid(winnr())
|
|
endif
|
|
if !g:spacevim_code_runner_focus
|
|
wincmd p
|
|
endif
|
|
endfunction
|
|
|
|
function! s:insert() abort
|
|
call inputsave()
|
|
let input = input('input >')
|
|
if !empty(input) && s:runner_status.is_running == 1
|
|
call s:JOB.send(s:runner_jobid, input)
|
|
endif
|
|
normal! :
|
|
call inputrestore()
|
|
endfunction
|
|
|
|
function! s:async_run(runner, ...) abort
|
|
if type(a:runner) == type('')
|
|
" the runner is a string, the %s will be replaced as a file name.
|
|
try
|
|
let cmd = printf(a:runner, get(s:, 'selected_file', bufname('%')))
|
|
catch
|
|
let cmd = a:runner
|
|
endtry
|
|
call s:LOGGER.info(' cmd:' . string(cmd))
|
|
call s:BUFFER.buf_set_lines(s:code_runner_bufnr, s:runner_lines , -1, 0, ['[Running] ' . cmd, '', repeat('-', 20)])
|
|
let s:runner_lines += 3
|
|
let s:start_time = reltime()
|
|
let opts = get(a:000, 0, {})
|
|
let s:runner_jobid = s:JOB.start(cmd,extend({
|
|
\ 'on_stdout' : function('s:on_stdout'),
|
|
\ 'on_stderr' : function('s:on_stderr'),
|
|
\ 'on_exit' : function('s:on_exit'),
|
|
\ }, opts))
|
|
elseif type(a:runner) ==# type([]) && len(a:runner) ==# 2
|
|
" the runner is a list with two items
|
|
" the first item is compile cmd, and the second one is running cmd.
|
|
|
|
let s:target = s:FILE.unify_path(tempname(), ':p')
|
|
let dir = fnamemodify(s:target, ':h')
|
|
if !isdirectory(dir)
|
|
call mkdir(dir, 'p')
|
|
endif
|
|
if type(a:runner[0]) == type({})
|
|
if type(a:runner[0].exe) == type(function('tr'))
|
|
let exe = call(a:runner[0].exe, [])
|
|
elseif type(a:runner[0].exe) ==# type('')
|
|
let exe = [a:runner[0].exe]
|
|
endif
|
|
let usestdin = get(a:runner[0], 'usestdin', 0)
|
|
let compile_cmd = exe + [get(a:runner[0], 'targetopt', '')] + [s:target]
|
|
if usestdin
|
|
let compile_cmd = compile_cmd + a:runner[0].opt
|
|
else
|
|
let compile_cmd = compile_cmd + a:runner[0].opt + [get(s:, 'selected_file', bufname('%'))]
|
|
endif
|
|
elseif type(a:runner[0]) ==# type('')
|
|
let usestdin = 0
|
|
let compile_cmd = substitute(printf(a:runner[0], bufname('%')), '#TEMP#', s:target, 'g')
|
|
endif
|
|
if type(compile_cmd) == type([])
|
|
let compile_cmd_info = string(compile_cmd + (usestdin ? ['STDIN'] : []))
|
|
else
|
|
let compile_cmd_info = compile_cmd . (usestdin ? ' STDIN' : '')
|
|
endif
|
|
call s:BUFFER.buf_set_lines(s:code_runner_bufnr, s:runner_lines , -1, 0, [
|
|
\ '[Compile] ' . compile_cmd_info,
|
|
\ '[Running] ' . s:target,
|
|
\ '',
|
|
\ repeat('-', 20)])
|
|
let s:runner_lines += 4
|
|
let s:start_time = reltime()
|
|
if type(compile_cmd) == type('') || (type(compile_cmd) == type([]) && executable(get(compile_cmd, 0, '')))
|
|
let s:runner_jobid = s:JOB.start(compile_cmd,{
|
|
\ 'on_stdout' : function('s:on_stdout'),
|
|
\ 'on_stderr' : function('s:on_stderr'),
|
|
\ 'on_exit' : function('s:on_compile_exit'),
|
|
\ })
|
|
if usestdin && s:runner_jobid > 0
|
|
let range = get(a:runner[0], 'range', [1, '$'])
|
|
call s:JOB.send(s:runner_jobid, call('getline', range))
|
|
call s:JOB.chanclose(s:runner_jobid, 'stdin')
|
|
endif
|
|
else
|
|
let exe = get(compile_cmd, 0, '')
|
|
call s:BUFFER.buf_set_lines(s:code_runner_bufnr, s:runner_lines , -1, 0, [exe . ' is not executable, make sure ' . exe . ' is in your PATH'])
|
|
endif
|
|
elseif type(a:runner) == type({})
|
|
" the runner is a dict
|
|
" keys:
|
|
" exe : function, return a cmd list
|
|
" string
|
|
" usestdin: true, use stdin
|
|
" false, use file name
|
|
" range: empty, whole buffer
|
|
" getline(a, b)
|
|
if type(a:runner.exe) == type(function('tr'))
|
|
let exe = call(a:runner.exe, [])
|
|
elseif type(a:runner.exe) ==# type('')
|
|
let exe = [a:runner.exe]
|
|
endif
|
|
let usestdin = get(a:runner, 'usestdin', 0)
|
|
if usestdin
|
|
let cmd = exe + a:runner.opt
|
|
else
|
|
let cmd = exe + a:runner.opt + [get(s:, 'selected_file', bufname('%'))]
|
|
endif
|
|
call s:LOGGER.info(' cmd:' . string(cmd))
|
|
call s:BUFFER.buf_set_lines(s:code_runner_bufnr, s:runner_lines , -1, 0, ['[Running] ' . join(cmd) . (usestdin ? ' STDIN' : ''), '', repeat('-', 20)])
|
|
let s:runner_lines += 3
|
|
let s:start_time = reltime()
|
|
if !empty(exe) && executable(exe[0])
|
|
let s:runner_jobid = s:JOB.start(cmd,{
|
|
\ 'on_stdout' : function('s:on_stdout'),
|
|
\ 'on_stderr' : function('s:on_stderr'),
|
|
\ 'on_exit' : function('s:on_exit'),
|
|
\ })
|
|
if usestdin && s:runner_jobid > 0
|
|
let range = get(a:runner, 'range', [1, '$'])
|
|
call s:JOB.send(s:runner_jobid, call('getline', range))
|
|
call s:JOB.chanclose(s:runner_jobid, 'stdin')
|
|
endif
|
|
else
|
|
call s:BUFFER.buf_set_lines(s:code_runner_bufnr, s:runner_lines , -1, 0, [exe . ' is not executable, make sure ' . exe . ' is in your PATH'])
|
|
endif
|
|
endif
|
|
if s:runner_jobid > 0
|
|
let s:runner_status = {
|
|
\ 'is_running' : 1,
|
|
\ 'has_errors' : 0,
|
|
\ 'exit_code' : 0
|
|
\ }
|
|
endif
|
|
endfunction
|
|
|
|
function! s:on_compile_exit(id, data, event) abort
|
|
if a:id !=# s:runner_jobid
|
|
" make sure the compile exit callback is for current compile command.
|
|
return
|
|
endif
|
|
if a:data == 0
|
|
let s:runner_jobid = s:JOB.start(s:target,{
|
|
\ 'on_stdout' : function('s:on_stdout'),
|
|
\ 'on_stderr' : function('s:on_stderr'),
|
|
\ 'on_exit' : function('s:on_exit'),
|
|
\ })
|
|
if s:runner_jobid > 0
|
|
let s:runner_status = {
|
|
\ 'is_running' : 1,
|
|
\ 'has_errors' : 0,
|
|
\ 'exit_code' : 0
|
|
\ }
|
|
endif
|
|
else
|
|
let s:end_time = reltime(s:start_time)
|
|
let s:runner_status.is_running = 0
|
|
let s:runner_status.exit_code = a:data
|
|
let done = ['', '[Done] exited with code=' . a:data . ' in ' . s:STRING.trim(reltimestr(s:end_time)) . ' seconds']
|
|
call s:BUFFER.buf_set_lines(s:code_runner_bufnr, s:runner_lines , s:runner_lines + 1, 0, done)
|
|
endif
|
|
call s:update_statusline()
|
|
endfunction
|
|
|
|
function! s:update_statusline() abort
|
|
redrawstatus!
|
|
endfunction
|
|
|
|
function! SpaceVim#plugins#runner#reg_runner(ft, runner) abort
|
|
let s:runners[a:ft] = a:runner
|
|
let desc = printf('%-10S', a:ft) . string(a:runner)
|
|
let cmd = "call SpaceVim#plugins#runner#set_language('" . a:ft . "')"
|
|
call add(g:unite_source_menu_menus.RunnerLanguage.command_candidates, [desc,cmd])
|
|
endfunction
|
|
|
|
function! SpaceVim#plugins#runner#get(ft) abort
|
|
return deepcopy(get(s:runners, a:ft , ''))
|
|
endfunction
|
|
|
|
" this func should support specific a runner
|
|
" the runner can be a string
|
|
function! SpaceVim#plugins#runner#open(...) abort
|
|
call s:stop_runner()
|
|
let s:runner_jobid = 0
|
|
let s:runner_lines = 0
|
|
let s:runner_status = {
|
|
\ 'is_running' : 0,
|
|
\ 'has_errors' : 0,
|
|
\ 'exit_code' : 0
|
|
\ }
|
|
let s:selected_language = &filetype
|
|
let runner = get(a:000, 0, get(s:runners, s:selected_language, ''))
|
|
let opts = get(a:000, 1, {})
|
|
if !empty(runner)
|
|
call s:open_win()
|
|
call s:async_run(runner, opts)
|
|
call s:update_statusline()
|
|
else
|
|
let s:selected_language = get(s:, 'selected_language', '')
|
|
endif
|
|
endfunction
|
|
|
|
" @vimlint(EVL103, 1, a:job_id)
|
|
" @vimlint(EVL103, 1, a:data)
|
|
" @vimlint(EVL103, 1, a:event)
|
|
function! s:on_stdout(job_id, data, event) abort
|
|
if a:job_id !=# s:runner_jobid
|
|
" that means, a new runner has been opennd
|
|
" this is previous runner exit_callback
|
|
return
|
|
endif
|
|
if bufexists(s:code_runner_bufnr)
|
|
if s:SYS.isWindows
|
|
let data = map(a:data, 'substitute(v:val, "\r$", "", "g")')
|
|
else
|
|
let data = a:data
|
|
endif
|
|
call s:BUFFER.buf_set_lines(s:code_runner_bufnr, s:runner_lines , s:runner_lines + 1, 0, data)
|
|
endif
|
|
let s:runner_lines += len(data)
|
|
if s:winid >= 0
|
|
call s:VIM.win_set_cursor(s:winid, [s:VIM.buf_line_count(s:code_runner_bufnr), 1])
|
|
endif
|
|
call s:update_statusline()
|
|
endfunction
|
|
|
|
function! s:on_stderr(job_id, data, event) abort
|
|
if a:job_id !=# s:runner_jobid
|
|
" that means, a new runner has been opennd
|
|
" this is previous runner exit_callback
|
|
return
|
|
endif
|
|
let s:runner_status.has_errors = 1
|
|
if bufexists(s:code_runner_bufnr)
|
|
call s:BUFFER.buf_set_lines(s:code_runner_bufnr, s:runner_lines , s:runner_lines + 1, 0, a:data)
|
|
endif
|
|
let s:runner_lines += len(a:data)
|
|
if s:winid >= 0
|
|
call s:VIM.win_set_cursor(s:winid, [s:VIM.buf_line_count(s:code_runner_bufnr), 1])
|
|
endif
|
|
call s:update_statusline()
|
|
endfunction
|
|
|
|
function! s:on_exit(job_id, data, event) abort
|
|
if a:job_id !=# s:runner_jobid
|
|
" that means, a new runner has been opennd
|
|
" this is previous runner exit_callback
|
|
return
|
|
endif
|
|
let s:end_time = reltime(s:start_time)
|
|
let s:runner_status.is_running = 0
|
|
let s:runner_status.exit_code = a:data
|
|
let done = ['', '[Done] exited with code=' . a:data . ' in ' . s:STRING.trim(reltimestr(s:end_time)) . ' seconds']
|
|
if bufexists(s:code_runner_bufnr)
|
|
call s:BUFFER.buf_set_lines(s:code_runner_bufnr, s:runner_lines , s:runner_lines + 1, 0, done)
|
|
call s:VIM.win_set_cursor(s:winid, [s:VIM.buf_line_count(s:code_runner_bufnr), 1])
|
|
call s:update_statusline()
|
|
endif
|
|
endfunction
|
|
" @vimlint(EVL103, 0, a:job_id)
|
|
" @vimlint(EVL103, 0, a:data)
|
|
" @vimlint(EVL103, 0, a:event)
|
|
|
|
|
|
function! SpaceVim#plugins#runner#status() abort
|
|
let running_nr = len(filter(values(s:task_status), 'v:val.is_running')) + s:runner_status.is_running
|
|
let running_done = len(filter(values(s:task_status), '!v:val.is_running'))
|
|
return printf(' %s running, %s done', running_nr, running_done)
|
|
endfunction
|
|
|
|
function! s:close() abort
|
|
call s:stop_runner()
|
|
if s:code_runner_bufnr != 0 && bufexists(s:code_runner_bufnr)
|
|
exe 'bd ' s:code_runner_bufnr
|
|
endif
|
|
endfunction
|
|
|
|
function! s:stop_runner() abort
|
|
if s:runner_status.is_running == 1
|
|
call s:JOB.stop(s:runner_jobid)
|
|
endif
|
|
endfunction
|
|
|
|
function! SpaceVim#plugins#runner#select_file() abort
|
|
let s:runner_lines = 0
|
|
let s:runner_status = {
|
|
\ 'is_running' : 0,
|
|
\ 'is_exit' : 0,
|
|
\ 'has_errors' : 0,
|
|
\ 'exit_code' : 0
|
|
\ }
|
|
let s:selected_file = browse(0,'select a file to run', getcwd(), '')
|
|
let runner = get(a:000, 0, get(s:runners, &filetype, ''))
|
|
let s:selected_language = &filetype
|
|
if !empty(runner)
|
|
call s:LOGGER.info('Code runner startting:')
|
|
call s:LOGGER.info('selected file :' . s:selected_file)
|
|
call s:open_win()
|
|
call s:async_run(runner)
|
|
call s:update_statusline()
|
|
endif
|
|
endfunction
|
|
|
|
let g:unite_source_menu_menus =
|
|
\ get(g:,'unite_source_menu_menus',{})
|
|
let g:unite_source_menu_menus.RunnerLanguage = {'description':
|
|
\ 'Custom mapped keyboard shortcuts [SPC] p p'}
|
|
let g:unite_source_menu_menus.RunnerLanguage.command_candidates =
|
|
\ get(g:unite_source_menu_menus.RunnerLanguage,'command_candidates', [])
|
|
|
|
function! SpaceVim#plugins#runner#select_language() abort
|
|
" @todo use denite or unite to select language
|
|
" and set the s:selected_language
|
|
" the all language is keys(s:runners)
|
|
if SpaceVim#layers#isLoaded('denite')
|
|
Denite menu:RunnerLanguage
|
|
elseif SpaceVim#layers#isLoaded('leaderf')
|
|
Leaderf menu --name RunnerLanguage
|
|
endif
|
|
endfunction
|
|
|
|
function! SpaceVim#plugins#runner#set_language(lang) abort
|
|
" @todo use denite or unite to select language
|
|
" and set the s:selected_language
|
|
" the all language is keys(s:runners)
|
|
let s:selected_language = a:lang
|
|
endfunction
|
|
|
|
|
|
function! SpaceVim#plugins#runner#run_task(task) abort
|
|
let isBackground = get(a:task, 'isBackground', 0)
|
|
if !empty(a:task)
|
|
let cmd = get(a:task, 'command', '')
|
|
let args = get(a:task, 'args', [])
|
|
let opts = get(a:task, 'options', {})
|
|
if !empty(args) && !empty(cmd)
|
|
let cmd = cmd . ' ' . join(args, ' ')
|
|
endif
|
|
let opt = {}
|
|
if !empty(opts) && has_key(opts, 'cwd') && !empty(opts.cwd)
|
|
call extend(opt, {'cwd' : opts.cwd})
|
|
endif
|
|
if !empty(opts) && has_key(opts, 'env') && !empty(opts.env)
|
|
call extend(opt, {'env' : opts.env})
|
|
endif
|
|
let problemMatcher = get(a:task, 'problemMatcher', {})
|
|
if isBackground
|
|
call s:run_backgroud(cmd, opt, problemMatcher)
|
|
else
|
|
call SpaceVim#plugins#runner#open(cmd, opt, problemMatcher)
|
|
endif
|
|
endif
|
|
endfunction
|
|
|
|
function! s:match_problems(output, matcher) abort
|
|
if has_key(a:matcher, 'pattern')
|
|
let pattern = a:matcher.pattern
|
|
let items = []
|
|
for line in a:output
|
|
let rst = matchlist(line, pattern.regexp)
|
|
let file = get(rst, get(pattern, 'file', 1), '')
|
|
let line = get(rst, get(pattern, 'line', 2), 1)
|
|
let column = get(rst, get(pattern, 'column', 3), 1)
|
|
let message = get(rst, get(pattern, 'message', 4), '')
|
|
if !empty(file)
|
|
call add(items, {
|
|
\ 'filename' : file,
|
|
\ 'lnum' : line,
|
|
\ 'col' : column,
|
|
\ 'text' : message,
|
|
\ })
|
|
endif
|
|
endfor
|
|
call setqflist([], 'r', {'title' : ' task output',
|
|
\ 'items' : items,
|
|
\ })
|
|
copen
|
|
copen
|
|
else
|
|
try
|
|
let olderrformat = &errorformat
|
|
if has_key(a:matcher, 'errorformat')
|
|
let &errorformat = a:matcher.errorformat
|
|
let cmd = 'noautocmd cexpr a:output'
|
|
exe cmd
|
|
call setqflist([], 'a', {'title' : ' task output'})
|
|
copen
|
|
endif
|
|
finally
|
|
let &errorformat = olderrformat
|
|
endtry
|
|
endif
|
|
endfunction
|
|
|
|
function! s:on_backgroud_stdout(job_id, data, event) abort
|
|
let data = get(s:task_stdout, 'task' . a:job_id, []) + a:data
|
|
let s:task_stdout['task' . a:job_id] = data
|
|
endfunction
|
|
|
|
function! s:on_backgroud_stderr(job_id, data, event) abort
|
|
let data = get(s:task_stderr, 'task' . a:job_id, []) + a:data
|
|
let s:task_stderr['task' . a:job_id] = data
|
|
endfunction
|
|
|
|
function! s:on_backgroud_exit(job_id, data, event) abort
|
|
let task_status = get(s:task_status, 'task' . a:job_id, {
|
|
\ 'is_running' : 0,
|
|
\ 'has_errors' : 0,
|
|
\ 'start_time' : 0,
|
|
\ 'exit_code' : 0
|
|
\ })
|
|
let end_time = reltime(task_status.start_time)
|
|
let task_problem_matcher = get(s:task_problem_matcher, 'task' . a:job_id, {})
|
|
if get(task_problem_matcher, 'useStdout', 0)
|
|
let output = get(s:task_stdout, 'task' . a:job_id, [])
|
|
else
|
|
let output = get(s:task_stderr, 'task' . a:job_id, [])
|
|
endif
|
|
if !empty(task_problem_matcher) && !empty(output)
|
|
call s:match_problems(output, task_problem_matcher)
|
|
endif
|
|
echo 'task finished with code=' . a:data . ' in ' . s:STRING.trim(reltimestr(end_time)) . ' seconds'
|
|
endfunction
|
|
|
|
function! s:run_backgroud(cmd, ...) abort
|
|
" how many tasks are running?
|
|
"
|
|
" echo 'tasks: 1 running, 2 done'
|
|
let running_nr = len(filter(values(s:task_status), 'v:val.is_running')) + 1
|
|
let running_done = len(filter(values(s:task_status), '!v:val.is_running'))
|
|
echo printf('tasks: %s running, %s done', running_nr, running_done)
|
|
let opts = get(a:000, 0, {})
|
|
" this line can not be removed.
|
|
let s:start_time = reltime()
|
|
let start_time = reltime()
|
|
let problemMatcher = get(a:000, 1, {})
|
|
if !has_key(problemMatcher, 'errorformat') && !has_key(problemMatcher, 'regexp')
|
|
call extend(problemMatcher, {'errorformat' : &errorformat})
|
|
endif
|
|
let task_id = s:JOB.start(a:cmd,extend({
|
|
\ 'on_stdout' : function('s:on_backgroud_stdout'),
|
|
\ 'on_exit' : function('s:on_backgroud_exit'),
|
|
\ }, opts))
|
|
call extend(s:task_problem_matcher, {'task' . task_id : problemMatcher})
|
|
call extend(s:task_status, {'task' . task_id : {
|
|
\ 'is_running' : 1,
|
|
\ 'has_errors' : 0,
|
|
\ 'start_time' : start_time,
|
|
\ 'exit_code' : 0
|
|
\ }})
|
|
endfunction
|
|
|
|
function! SpaceVim#plugins#runner#clear_tasks() abort
|
|
for taskid in keys(s:task_status)
|
|
if s:task_status[taskid].is_running ==# 1
|
|
call remove(s:task_status, taskid)
|
|
endif
|
|
endfor
|
|
endfunction
|