From 8b55955de44b512078af5fe0ac9f67a848381c5c Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 13 Jul 2023 00:00:35 +0800 Subject: [PATCH] feat(runner): rewrite code runner in lua --- .SpaceVim.d/types/vim.fn.lua | 26 + autoload/SpaceVim/plugins/runner.vim | 65 ++- .../neosnippet-snippets/neosnippets/lua.snip | 3 +- lua/spacevim/plugin/runner.lua | 516 ++++++++++++++++++ syntax/SpaceVimRunner.vim | 10 +- test/lua/test_job.lua | 10 +- 6 files changed, 618 insertions(+), 12 deletions(-) create mode 100644 .SpaceVim.d/types/vim.fn.lua create mode 100644 lua/spacevim/plugin/runner.lua diff --git a/.SpaceVim.d/types/vim.fn.lua b/.SpaceVim.d/types/vim.fn.lua new file mode 100644 index 000000000..3e6687f08 --- /dev/null +++ b/.SpaceVim.d/types/vim.fn.lua @@ -0,0 +1,26 @@ +-- Return an item that represents a time value. The item is a +-- list with items that depend on the system. +-- The item can be passed to `reltimestr()` to convert it to a +-- string or `reltimefloat()` to convert to a Float. +-- +-- Without an argument it returns the current "relative time", an +-- implementation-defined value meaningful only when used as an +-- argument to `reltime()`, `reltimestr()` and `reltimefloat()`. +-- +-- With one argument it returns the time passed since the time +-- specified in the argument. +-- With two arguments it returns the time passed between {start} +-- and {end}. +-- +-- The {start} and {end} arguments must be values returned by +-- reltime(). Returns zero on error. +-- +-- Can also be used as a `method`: +-- ```vim +-- GetStart()->reltime() +-- ``` +-- Note: `localtime()` returns the current (non-relative) time. +--- @param start? any[] +--- @param end_? any[] +--- @return any[] +function vim.fn.reltime(start, end_) end diff --git a/autoload/SpaceVim/plugins/runner.vim b/autoload/SpaceVim/plugins/runner.vim index 09e38e800..66a38b1f5 100644 --- a/autoload/SpaceVim/plugins/runner.vim +++ b/autoload/SpaceVim/plugins/runner.vim @@ -36,6 +36,62 @@ " } " < + +if has('nvim-0.9.0') && $USE_LUA_CODE_RUNEER == 1 + function! SpaceVim#plugins#runner#get(ft) abort + return luaeval('require("spacevim.plugin.runner").get(require("spacevim").eval("a:ft"))') + endfunction + function! SpaceVim#plugins#runner#open(...) abort + lua require("spacevim.plugin.runner").open( + \ unpack(require("spacevim").eval("a:000")) + \ ) + + endfunction + + function! SpaceVim#plugins#runner#reg_runner(ft, runner) abort + lua require("spacevim.plugin.runner").reg_runner( + \ require("spacevim").eval("a:ft"), + \ require("spacevim").eval("a:runner") + \ ) + + endfunction + + function! SpaceVim#plugins#runner#status() abort + return luaeval('require("spacevim.plugin.runner").status()') + endfunction + + function! SpaceVim#plugins#runner#close() abort + + lua require("spacevim.plugin.runner").close() + + endfunction + + function! SpaceVim#plugins#runner#select_file() abort + lua require("spacevim.plugin.runner").select_file() + endfunction + + function! SpaceVim#plugins#runner#select_language() abort + lua require("spacevim.plugin.runner").select_language() + endfunction + + function! SpaceVim#plugins#runner#set_language(lang) abort + + endfunction + + function! SpaceVim#plugins#runner#run_task(task) abort + + endfunction + + function! SpaceVim#plugins#runner#clear_tasks() abort + + endfunction + + finish +endif + + + + let s:runners = {} let s:JOB = SpaceVim#api#import('job') @@ -74,7 +130,8 @@ 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 + if s:code_runner_bufnr !=# 0 && bufexists(s:code_runner_bufnr) + \ && index(tabpagebuflist(), s:code_runner_bufnr) !=# -1 return endif botright split __runner__ @@ -281,15 +338,13 @@ function! SpaceVim#plugins#runner#open(...) abort \ 'has_errors' : 0, \ 'exit_code' : 0 \ } - let s:selected_language = &filetype - let runner = get(a:000, 0, get(s:runners, s:selected_language, '')) + let selected_language = &filetype + let runner = get(a:000, 0, get(s:runners, 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 diff --git a/bundle/neosnippet-snippets/neosnippets/lua.snip b/bundle/neosnippet-snippets/neosnippets/lua.snip index 31d500235..e8fc02b3e 100644 --- a/bundle/neosnippet-snippets/neosnippets/lua.snip +++ b/bundle/neosnippet-snippets/neosnippets/lua.snip @@ -2,10 +2,9 @@ snippet func abbr function name(args)...end options word - function ${1:#:function_name}(${2:#:argument}) -- {{{ + function ${1:#:function_name}(${2:#:argument}) ${0:TARGET} end - -- }}} snippet if options head diff --git a/lua/spacevim/plugin/runner.lua b/lua/spacevim/plugin/runner.lua new file mode 100644 index 000000000..978bfa23f --- /dev/null +++ b/lua/spacevim/plugin/runner.lua @@ -0,0 +1,516 @@ +--============================================================================= +-- M.lua +-- Copyright (c) 2016-2022 Wang Shidong & Contributors +-- Author: Wang Shidong < wsdjeg@outlook.com > +-- URL: https://spacevim.org +-- License: GPLv3 +--============================================================================= + +local M = {} + +local runners = {} + +local logger = require('spacevim.logger').derive('runner') +local job = require('spacevim.api').import('job') +local file = require('spacevim.api').import('file') +local str = require('spacevim.api').import('data.string') + +local code_runner_bufnr = 0 + +local winid = -1 + +local target = '' + +local runner_lines = 0 + +local runner_jobid = 0 + +local runner_status = { + is_running = false, + has_errors = false, + exit_code = 0, + exit_single = 0, +} + +local selected_file = '' +--- @type any[] +local start_time +--- @type any[] +local end_time + +local task_status = {} + +local task_stdout = {} + +local task_stderr = {} + +local task_problem_matcher = {} + +local selected_language = '' + +local function stop_runner() + if runner_status.is_running then + logger.debug('stop runner:' .. runner_jobid) + job.stop(runner_jobid) + end +end + +local function close_win() + stop_runner() + if code_runner_bufnr ~= 0 and vim.api.nvim_buf_is_valid(code_runner_bufnr) then + vim.cmd('bd ' .. code_runner_bufnr) + end +end + +local function insert() + vim.fn.inputsave() + local input = vim.fn.input('input >') + if vim.fn.empty(input) == 0 and runner_status.is_running then + job.send(runner_jobid, input) + end + vim.cmd('normal! :') + vim.fn.inputrestore() +end + +local function open_win() + if + code_runner_bufnr ~= 0 + and vim.api.nvim_buf_is_valid(code_runner_bufnr) + and vim.fn.index(vim.fn.tabpagebuflist(), code_runner_bufnr) ~= -1 + then + return + end + logger.debug('open code runner windows') + local previous_wind = vim.api.nvim_get_current_win() + vim.cmd('botright split __runner__') + code_runner_bufnr = vim.fn.bufnr('%') + local lines = vim.o.lines * 30 / 100 + vim.cmd('resize ' .. lines) + vim.cmd([[ + setlocal buftype=nofile bufhidden=wipe nobuflisted nolist noswapfile nowrap cursorline nospell nonu norelativenumber winfixheight nomodifiable + set filetype=SpaceVimRunner + ]]) + vim.api.nvim_buf_set_keymap(code_runner_bufnr, 'n', 'q', '', { + callback = close_win, + }) + vim.api.nvim_buf_set_keymap(code_runner_bufnr, 'n', 'i', '', { + callback = insert, + }) + vim.api.nvim_buf_set_keymap(code_runner_bufnr, 'n', '', '', { + callback = stop_runner, + }) + local id = vim.api.nvim_create_augroup('spacevim_runner', { + clear = true, + }) + vim.api.nvim_create_autocmd({ 'BufWipeout' }, { + group = id, + buffer = code_runner_bufnr, + callback = stop_runner, + }) + winid = vim.api.nvim_get_current_win() + if vim.g.spacevim_code_runner_focus == 0 then + vim.api.nvim_set_current_win(previous_wind) + end +end + +local function extend(t1, t2) + for k, v in pairs(t2) do + t1[k] = v + end +end + +local function update_statusline() + vim.cmd('redrawstatus!') +end + +local function on_stdout(id, data, event) + if id ~= runner_jobid then + return + end + if vim.api.nvim_buf_is_valid(code_runner_bufnr) then + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true) + vim.api.nvim_buf_set_lines(code_runner_bufnr, runner_lines, runner_lines + 1, false, data) + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false) + runner_lines = runner_lines + #data + if winid >= 0 then + vim.api.nvim_win_set_cursor(winid, { vim.api.nvim_buf_line_count(code_runner_bufnr), 1 }) + end + update_statusline() + end +end +local function on_stderr(id, data, event) + if id ~= runner_jobid then + return + end + runner_status.has_errors = true + if vim.api.nvim_buf_is_valid(code_runner_bufnr) then + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true) + vim.api.nvim_buf_set_lines(code_runner_bufnr, runner_lines, runner_lines + 1, false, data) + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false) + runner_lines = runner_lines + #data + if winid >= 0 then + vim.api.nvim_win_set_cursor(winid, { vim.api.nvim_buf_line_count(code_runner_bufnr), 1 }) + end + update_statusline() + end +end +local function on_exit(id, code, single) + if id ~= runner_jobid then + return + end + end_time = vim.fn.reltime(start_time) + runner_status.is_running = false + runner_status.exit_single = single + runner_status.exit_code = code + local done = { + '', + '[Done] exited with code=' .. code .. ', single=' .. single .. ' in ' .. str.trim( + vim.fn.reltimestr(end_time) + ) .. ' seconds', + } + if vim.api.nvim_buf_is_valid(code_runner_bufnr) then + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true) + vim.api.nvim_buf_set_lines(code_runner_bufnr, runner_lines, runner_lines + 1, false, done) + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false) + if winid >= 0 then + vim.api.nvim_win_set_cursor(winid, { vim.api.nvim_buf_line_count(code_runner_bufnr), 1 }) + end + update_statusline() + end +end + +local function merge_list(...) + local t = {} + + for _, tb in ipairs({ ... }) do + for _, v in ipairs(tb) do + table.insert(t, v) + end + end + return t +end + +local function on_compile_exit(id, code, single) + if id ~= runner_jobid then + return + end + if code == 0 and single == 0 then + runner_jobid = job.start(target, { + on_stdout = on_stdout, + on_stderr = on_stderr, + on_exit = on_exit, + }) + if runner_jobid > 0 then + runner_status = { + is_running = true, + has_errors = false, + exit_code = 0, + exit_single = 0, + } + end + else + end_time = vim.fn.reltime(start_time) + runner_status.is_running = false + runner_status.exit_code = code + runner_status.exit_single = single + local done = { + '', + '[Done] exited with code=' .. code .. ', single=' .. single .. ' in ' .. str.trim( + vim.fn.reltimestr(end_time) + ) .. ' seconds', + } + if vim.api.nvim_buf_is_valid(code_runner_bufnr) then + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true) + vim.api.nvim_buf_set_lines(code_runner_bufnr, runner_lines, runner_lines + 1, false, done) + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false) + if winid >= 0 then + vim.api.nvim_win_set_cursor(winid, { vim.api.nvim_buf_line_count(code_runner_bufnr), 1 }) + end + update_statusline() + end + end +end + +local function async_run(runner, ...) + if type(runner) == 'string' then + local cmd = runner + pcall(function() + local f + if selected_file ~= '' then + f = selected_file + else + f = vim.fn.bufname('%') + end + cmd = vim.fn.printf(runner, f) + end) + logger.info(' cmd:' .. cmd) + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true) + vim.api.nvim_buf_set_lines( + code_runner_bufnr, + runner_lines, + -1, + false, + { '[Running] ' .. cmd, '', vim.fn['repeat']('-', 20) } + ) + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false) + runner_lines = runner_lines + 3 + start_time = vim.fn.reltime() + local opts = select(1, ...) or {} + extend(opts, { + on_stdout = on_stdout, + on_stderr = on_stderr, + on_exit = on_exit, + }) + runner_jobid = job.start(cmd, opts) + elseif type(runner) == 'table' and #runner == 2 then + target = file.unify_path(vim.fn.tempname(), ':p') + local dir = vim.fn.fnamemodify(target, ':h') + if vim.fn.isdirectory(dir) == 0 then + vim.fn.mkdir(dir, 'p') + end + local compile_cmd + local usestdin + local compile_cmd_info + if type(runner[1]) == 'table' then + local exe + if type(runner[1].exe) == 'function' then + exe = runner[1].exe() + elseif type(runner[1].exe) == 'string' then + exe = { runner[1].exe } + end + usestdin = runner[1].usestdin or false + compile_cmd = merge_list(exe, { runner[1].targetopt or '' }, { target }, runner[1].opt) + if not usestdin then + local f + if selected_file == '' then + f = vim.fn.bufname('%') + else + f = selected_file + end + compile_cmd = merge_list(compile_cmd, { f }) + end + elseif type(runner[1]) == 'string' then + end + + if type(compile_cmd) == 'table' then + if usestdin then + compile_cmd_info = vim.inspect(merge_list(compile_cmd, { 'STDIN' })) + else + compile_cmd_info = vim.inspect(compile_cmd) + end + else + if usestdin then + compile_cmd_info = compile_cmd .. ' STDIN' + else + compile_cmd_info = compile_cmd + end + end + + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true) + vim.api.nvim_buf_set_lines( + code_runner_bufnr, + runner_lines, + -1, + false, + { '[Compile] ' .. compile_cmd_info, '[Running] ' .. target, '', vim.fn['repeat']('-', 20) } + ) + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false) + + runner_lines = runner_lines + 4 + start_time = vim.fn.reltime() + if + type(compile_cmd) == 'string' + or type(compile_cmd) == 'table' and vim.fn.executable(compile_cmd[1] or '') == 1 + then + runner_jobid = job.start(compile_cmd, { + on_stdout = on_stdout, + on_stderr = on_stderr, + on_exit = on_compile_exit, + }) + if usestdin and runner_jobid > 0 then + local range = runner[1].range or { 1, '$' } + job.send(runner_jobid, vim.fn.getline(unpack(range))) + job.chanclose(runner_jobid, 'stdin') + job.stop(runner_jobid) + end + else + local exe = compile_cmd[1] or '' + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true) + vim.api.nvim_buf_set_lines( + code_runner_bufnr, + runner_lines, + -1, + false, + { exe .. ' is not executable, make sure ' .. exe .. ' is in your PATH' } + ) + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false) + end + elseif type(runner) == 'table' then + local cmd = {} + if type(runner.exe) == 'function' then + cmd = merge_list(cmd, runner.exe()) + elseif type(runner.exe) == 'string' then + cmd = { runner.exe } + end + local usestdin = runner.usestdin or false + cmd = merge_list(cmd, runner.opt) + if not usestdin then + if selected_file == '' then + cmd = merge_list(cmd, vim.fn.bufname('%')) + else + cmd = merge_list(cmd, selected_file) + end + end + logger.info(' cmd:' .. vim.inspect(cmd)) + local running_command = table.concat(cmd, ' ') + if usestdin then + running_command = running_command .. ' STDIN' + end + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true) + vim.api.nvim_buf_set_lines( + code_runner_bufnr, + runner_lines, + -1, + false, + { '[Running] ' .. running_command, '', vim.fn['repeat']('-', 20) } + ) + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false) + runner_lines = runner_lines + 3 + start_time = vim.fn.reltime() + if vim.fn.empty(cmd) == 0 and vim.fn.executable(cmd[1]) == 1 then + runner_jobid = job.start(cmd, { + on_stdout = on_stdout, + on_stderr = on_stderr, + on_exit = on_exit, + }) + if usestdin and runner_jobid > 0 then + local range = runner.range or { 1, '$' } + -- if selected file is not empty + -- read the context from selected file. + local text + if selected_file == '' then + text = vim.fn.getline(unpack(range)) + else + text = vim.fn.readfile(selected_file, '') + end + job.send(runner_jobid, text) + job.chanclose(runner_jobid, 'stdin') + job.stop(runner_jobid) + end + else + local exe = cmd[1] or '' + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true) + vim.api.nvim_buf_set_lines( + code_runner_bufnr, + runner_lines, + -1, + false, + { exe .. ' is not executable, make sure ' .. exe .. ' is in your PATH' } + ) + vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false) + end + end + + if runner_jobid > 0 then + runner_status = { + is_running = true, + has_errors = false, + exit_code = 0, + exit_single = 0, + } + end +end + +function M.open(...) + stop_runner() + runner_jobid = 0 + runner_lines = 0 + runner_status = { + is_running = false, + has_errors = false, + exit_code = 0, + exit_single = 0, + } + local language = vim.o.filetype + local runner = select(1, ...) or runners[language] or '' + local opts = select(2, ...) or {} + logger.debug('runner is:\n' .. vim.inspect(runner)) + logger.debug('opt is:\n' .. vim.inspect(opts)) + if vim.fn.empty(runner) == 0 then + open_win() + async_run(runner, opts) + update_statusline() + else + end +end + +function M.reg_runner(ft, runner) + runners[ft] = runner +end + +function M.status() + local running_nr = 0 + local running_done = 0 + for _, v in ipairs(task_status) do + if v.is_running then + running_nr = running_nr + 1 + else + running_done = running_done + 1 + end + end + + if runner_status.is_running then + running_nr = running_nr + 1 + end + return string.format(' %s running, %s done', running_nr, running_done) +end + +function M.close() + close_win() +end + +function M.select_file() + runner_lines = 0 + runner_status = { + is_running = false, + has_errors = false, + exit_code = 0, + exit_single = 0 + } + + if vim.loop.os_uname().sysname == 'Windows_NT' then + -- what the fuck, why need trim? + -- because powershell comamnd output has `\n` at the end, and filetype detection failed. + selected_file = str.trim(vim.fn.system({'powershell', "Add-Type -AssemblyName System.windows.forms|Out-Null;$f=New-Object System.Windows.Forms.OpenFileDialog;$f.Filter='Model Files All files (*.*)|*.*';$f.showHelp=$true;$f.ShowDialog()|Out-Null;$f.FileName"})) + end + + if selected_file == '' then + logger.debug('file to get selected filename!') + return + else + logger.debug('selected file is:' .. selected_file) + local ft = vim.filetype.match({filename = selected_file}) + if not ft then + logger.debug('failed to detect filetype of selected file:' .. selected_file) + return + end + local runner = runners[ft] + logger.info(vim.inspect(runner)) + if runner then + open_win() + async_run(runner) + update_statusline() + end + end + + + + +end + + +function M.select_language() + +end + +return M diff --git a/syntax/SpaceVimRunner.vim b/syntax/SpaceVimRunner.vim index 8f0f0f5c1..186abe7df 100644 --- a/syntax/SpaceVimRunner.vim +++ b/syntax/SpaceVimRunner.vim @@ -7,17 +7,23 @@ syn match KeyBindings /\[Running\]/ syn match KeyBindings /\[Compile\]/ syn match RunnerCmd /\(\[Running\]\ \)\@<=.*/ syn match RunnerCmd /\(\[Compile\]\ \)\@<=.*/ -syn match DoneSucceeded /\[Done]\(\ exited\ with\ code=0\)\@=/ +syn match DoneSucceeded /\[Done]\(\ exited\ with\ code=0, single=0\)\@=/ +syn match DoneSucceeded /\[Done]\(\ exited\ with\ code=0 in\)\@=/ syn match DoneFailed /\[Done]\(\ exited\ with\ code=[^0]\)\@=/ +syn match DoneFailed /\[Done]\(\ exited\ with\ code=0, single=[^0]\)\@=/ syn match ExitCode /\(\[Done\]\ exited\ with \)\@<=code=0/ -syn match ExitCodeFailed /\(\[Done\]\ exited\ with \)\@<=code=[^0]/ +syn match ExitCodeFailed /\(\[Done\]\ exited\ with \)\@<=code=[1-9]\d*/ +syn match SingleCode /single=0/ +syn match SingleCodeFailed /single=[^0]/ hi def link RunnerCmd Comment hi def link KeyBindings String hi def link DoneSucceeded String hi def link DoneFailed WarningMsg hi def link ExitCode MoreMsg +hi def link SingleCode MoreMsg hi def link ExitCodeFailed WarningMsg +hi def link SingleCodeFailed WarningMsg let s:shellcmd_colors = \ [ \ '#6c6c6c', '#ff6666', '#66ff66', '#ffd30a', diff --git a/test/lua/test_job.lua b/test/lua/test_job.lua index c38cbacbe..b232f7905 100644 --- a/test/lua/test_job.lua +++ b/test/lua/test_job.lua @@ -1,6 +1,6 @@ local job = require('spacevim.api.job') -local jobid = job.start('echo 1', { +local jobid = job.start({'lua53', '-'}, { on_stdout = function(id, data, event) vim.print(id) vim.print(vim.inspect(data)) @@ -19,5 +19,9 @@ local jobid = job.start('echo 1', { }) --- job.send(jobid, 'hello world') --- job.stop(jobid) +job.send(jobid, 'print(1)\n') +job.send(jobid, 'print(1)\n') +job.send(jobid, 'print(1)\n') +job.send(jobid, 'print(1)\n') +job.chanclose(jobid, 'stdin') +job.stop(jobid)