From 2b2c2aa5f6e812f8020bb53d001cc97c09a53b4f Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 5 Aug 2023 20:18:25 +0800 Subject: [PATCH] feat(tasks): implement tasks.lua --- autoload/SpaceVim/plugins/runner.vim | 2 +- autoload/SpaceVim/plugins/tasks.vim | 25 +++ lua/spacevim/api/cmdlinemenu.lua | 102 +++++++++++ lua/spacevim/plugin/projectmanager.lua | 2 +- lua/spacevim/plugin/runner.lua | 183 ++++++++++++++++++-- lua/spacevim/plugin/tasks.lua | 231 +++++++++++++++++++++++++ 6 files changed, 531 insertions(+), 14 deletions(-) create mode 100644 lua/spacevim/api/cmdlinemenu.lua create mode 100644 lua/spacevim/plugin/tasks.lua diff --git a/autoload/SpaceVim/plugins/runner.vim b/autoload/SpaceVim/plugins/runner.vim index 66a38b1f5..eb4f8e8e1 100644 --- a/autoload/SpaceVim/plugins/runner.vim +++ b/autoload/SpaceVim/plugins/runner.vim @@ -37,7 +37,7 @@ " < -if has('nvim-0.9.0') && $USE_LUA_CODE_RUNEER == 1 +if has('nvim-0.9.0') function! SpaceVim#plugins#runner#get(ft) abort return luaeval('require("spacevim.plugin.runner").get(require("spacevim").eval("a:ft"))') endfunction diff --git a/autoload/SpaceVim/plugins/tasks.vim b/autoload/SpaceVim/plugins/tasks.vim index 6b3bec4e5..d904e5b30 100644 --- a/autoload/SpaceVim/plugins/tasks.vim +++ b/autoload/SpaceVim/plugins/tasks.vim @@ -5,6 +5,31 @@ " URL: https://spacevim.org " License: GPLv3 "============================================================================= +if has('nvim-0.9.0') + function! SpaceVim#plugins#tasks#get() abort + return luaeval('require("spacevim.plugin.tasks").get()') + endfunction + function! SpaceVim#plugins#tasks#list() abort + lua require("spacevim.plugin.tasks").list() + endfunction + function! SpaceVim#plugins#tasks#edit(...) abort + lua require("spacevim.plugin.tasks").edit( + \ unpack(require("spacevim").eval("a:000")) + \ ) + endfunction + function! SpaceVim#plugins#tasks#get_tasks() abort + return luaeval('require("spacevim.plugin.tasks").get_tasks()') + endfunction + + function! SpaceVim#plugins#tasks#complete(...) abort + endfunction + function! SpaceVim#plugins#tasks#reg_provider(provider) abort + lua require("spacevim.plugin.tasks").reg_provider( + \ require("spacevim").eval("a:provider") + \ ) + endfunction + finish +endif if exists('s:is_loaded') finish diff --git a/lua/spacevim/api/cmdlinemenu.lua b/lua/spacevim/api/cmdlinemenu.lua new file mode 100644 index 000000000..5054992a5 --- /dev/null +++ b/lua/spacevim/api/cmdlinemenu.lua @@ -0,0 +1,102 @@ +local M = {} + +local function parse_input(char) + if char == 27 then + return '' + else + return char + end +end + +local function next_item(list, item) + local id = vim.fn.index(list, item) + if id == #list then + return list[1] + else + return list[id] + end +end + +local function previous_item(list, item) + local id = vim.fn.index(list, item) + if id == 0 then + return list[#list] + else + return list[id] + end +end + +local function parse_items(items) + local is = {} + for _, item in pairs(items) do + local id = vim.fn.index(items, item) + 1 + is[id] = item + is[id][1] = '(' .. id .. ')' .. item[1] + end + return is +end + +function M.menu(items) + local cancelled = false + local saved_more = vim.o.more + local saved_cmdheight = vim.o.cmdheight + vim.o.more = false + items = parse_items(items) + vim.o.cmdheight = #items + 1 + vim.cmd('redrawstatus!') + local selected = '1' + local exit = false + local indent = string.rep(' ', 7, '') + while not exit do + local menu = 'Cmdline menu: Use j/k/enter and the shortcuts indicated\n' + for id, _ in pairs(items) do + local m = items[id] + if type(m) == 'table' then + m = m[1] + end + if id == selected then + menu = menu .. indent .. '>' .. items[id][1] .. '\n' + else + menu = menu .. indent .. ' ' .. items[id][1] .. '\n' + end + end + + vim.cmd('redraw!') + vim.api.nvim_echo({ { string.sub(menu, 1, #menu - 2), 'Nornal' } }, false, {}) + local nr = vim.fn.getchar() + if parse_input(nr) == #'' or nr == 3 then + exit = false + cancelled = true + vim.cmd('normal! :') + elseif vim.fn.index(vim.fn.keys(items), vim.fn.nr2char(nr)) ~= -1 or nr == 13 then + if nr ~= 13 then + selected = vim.fn.nr2char(nr) + end + local value = items[selected][1] + vim.cmd('normal! :') + if vim.fn.type(value) == 2 then + local args = vim.fn.get(items[selected], 2, {}) + pcall(value, unpack(args)) + elseif type(value) == 'string' then + vim.cmd(value) + end + exit = true + elseif vim.fn.nr2char(nr) == 'j' or nr == 9 then + selected = next_item(vim.fn.keys(items), selected) + vim.cmd('normal! :') + elseif vim.fn.nr2char(nr) == 'k' then -- or nr == "\" + selected = previous_item(vim.fn.keys(items), selected) + vim.cmd('normal! :') + else + vim.cmd('normal! :') + end + end + vim.o.more = saved_more + vim.o.cmdheight = saved_cmdheight + vim.cmd('redraw!') + if cancelled then + vim.api.nvim_echo({ { 'cancelled!', 'Normal' } }, false, {}) + end +end + +return M diff --git a/lua/spacevim/plugin/projectmanager.lua b/lua/spacevim/plugin/projectmanager.lua index 773812668..e8ef5315d 100644 --- a/lua/spacevim/plugin/projectmanager.lua +++ b/lua/spacevim/plugin/projectmanager.lua @@ -405,7 +405,7 @@ function M.current_root() or vim.fn.empty(bufname) == 1 or bufname:match('^neo%-tree') -- this is for neo-tree.nvim then - return + return fn.getcwd() end if table.concat(sp_opt.project_rooter_patterns, ':') diff --git a/lua/spacevim/plugin/runner.lua b/lua/spacevim/plugin/runner.lua index 04dab765e..cef0a7079 100644 --- a/lua/spacevim/plugin/runner.lua +++ b/lua/spacevim/plugin/runner.lua @@ -14,6 +14,7 @@ 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 nt = require('spacevim.api.notify') local code_runner_bufnr = 0 @@ -55,6 +56,12 @@ local function stop_runner() end end +-- tbl_extend should provide default behavior + +local function tbl_extend(t1, t2) + return vim.tbl_extend('force', t1, t2) +end + local function close_win() stop_runner() if code_runner_bufnr ~= 0 and vim.api.nvim_buf_is_valid(code_runner_bufnr) then @@ -475,13 +482,16 @@ function M.select_file() is_running = false, has_errors = false, exit_code = 0, - exit_single = 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"})) + 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 @@ -489,7 +499,7 @@ function M.select_file() return else logger.debug('selected file is:' .. selected_file) - local ft = vim.filetype.match({filename = 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 @@ -502,20 +512,169 @@ function M.select_file() update_statusline() end end - - - - -end - - -function M.select_language() - end +function M.select_language() end function M.get(ft) return runners[ft] or '' end +local function match_problems(output, matcher) + if matcher.pattern then + local pattern = matcher.pattern + local items = {} + for _, line in ipairs(output) do + local rst = vim.fn.matchlist(line, pattern.regexp) + local f_idx = 2 + if pattern.file then + f_idx = pattern.file + 1 + end + local f = rst[f_idx] or '' + local l_idx = 3 + if pattern.line then + l_idx = pattern.line + 1 + end + local l = rst[l_idx] or 1 + local c_idx = 4 + if pattern.column then + c_idx = pattern.column + 1 + end + local column = rst[c_idx] or 1 + local m_idx = 5 + if pattern.message then + m_idx = pattern.message + 1 + end + local message = rst[m_idx] or '' + if #f > 0 then + table.insert(items, { + filename = f, + lnum = l, + col = column, + text = message, + }) + end + end + vim.fn.setqflist({}, 'r', { title = ' task output', items = items }) + vim.cmd('copen') + else + local olderrformat = vim.o.errorformat + pcall(function() + vim.o.errorformat = matcher.errorformat + vim.g._spacevim_task_output = output + vim.cmd('noautocmd cexpr g:_spacevim_task_output') + vim.fn.setqflist({}, 'a', { title = ' task output' }) + vim.cmd('copen') + vim.g._spacevim_task_output = nil + end) + vim.o.errorformat = olderrformat + end +end + +local function on_backgroud_stdout(id, data, event) + local d = task_stdout['task' .. id] or {} + + for _, v in ipairs(data) do + table.insert(d, v) + end + + task_stdout['task' .. id] = d +end + +local function on_backgroud_stderr(id, data, event) + local d = task_stderr['task' .. id] or {} + + for _, v in ipairs(data) do + table.insert(d, v) + end + + task_stderr['task' .. id] = d +end + +local function on_backgroud_exit(id, code, single) + local status = task_status['task' .. id] + or { + is_running = false, + has_errors = false, + start_time = 0, + exit_code = 0, + } + local end_time = vim.fn.reltime(status.start_time) + local problem_matcher = task_problem_matcher['task' .. id] or {} + local output + if problem_matcher.useStdout then + output = task_stdout['task' .. id] or {} + else + output = task_stderr['task' .. id] or {} + end + if not vim.tbl_isempty(problem_matcher) and not vim.tbl_isempty(output) then + match_problems(output, problem_matcher) + end + nt.notify( + 'task finished with code=' + .. code + .. ' in ' + .. str.trim(vim.fn.reltimestr(end_time)) + .. ' seconds' + ) +end + +local function run_backgroud(cmd, ...) + local running_nr = 0 + local running_done = 0 + for _, v in pairs(task_status) do + if v.is_running then + running_nr = running_nr + 1 + else + running_done = running_done + 1 + end + end + nt.notify(string.format('tasks: %s running, %s done', running_nr, running_done)) + local opts = select(1, ...) or {} + start_time = vim.fn.reltime() + local problemMatcher = select(2, ...) or {} + if not problemMatcher.errorformat and not problemMatcher.regexp then + problemMatcher = tbl_extend(problemMatcher, { errorformat = vim.o.errorformat }) + end + opts.on_stdout = on_backgroud_stdout + opts.on_stderr = on_backgroud_stderr + opts.on_exit = on_backgroud_exit + local task_id = job.start(cmd, opts) + task_problem_matcher = tbl_extend(task_problem_matcher, { ['task' .. task_id] = problemMatcher }) + logger.debug('task_problem_matcher is:\n' .. vim.inspect(task_problem_matcher)) + task_status = tbl_extend(task_status, { + ['task' .. task_id] = { + is_running = true, + has_errors = false, + start_time = start_time, + exit_code = 0, + }, + }) +end + +function M.run_task(task) + local isBackground = task.isBackground or false + if not vim.tbl_isempty(task) then + local cmd = task.command or '' + local args = task.args or {} + local opts = task.options or {} + if #args > 0 and #cmd > 0 then + cmd = cmd .. ' ' .. table.concat(args, ' ') + end + local opt = {} + if opts.cwd then + opt.cwd = opts.cwd + end + if opts.env then + opt = tbl_extend(opt, { env = opts.env }) + end + local problemMatcher = task.problemMatcher or {} + if isBackground then + run_backgroud(cmd, opt, problemMatcher) + else + M.open(cmd, opt, problemMatcher) + end + end +end + return M diff --git a/lua/spacevim/plugin/tasks.lua b/lua/spacevim/plugin/tasks.lua new file mode 100644 index 000000000..ff39ac29f --- /dev/null +++ b/lua/spacevim/plugin/tasks.lua @@ -0,0 +1,231 @@ +--============================================================================= +-- tasks.lua +-- Copyright (c) 2016-2022 Wang Shidong & Contributors +-- Author: Wang Shidong < wsdjeg@outlook.com > +-- URL: https://spacevim.org +-- License: GPLv3 +--============================================================================= + + +local M = {} + +local selected_task = {} + +local task_config = {} + +local task_viewer_bufnr = -1 + +local variables = {} + +local providers = {} + +-- load apis + +local file = require('spacevim.api.file') +local toml = require('spacevim.api.data.toml') +local sys = require('spacevim.api.system') +local log = require('spacevim.logger').derive('task') +local menu = require('spacevim.api.cmdlinemenu') + +local function load() + log.debug('start to load task config:') + local global_conf = {} + local local_conf = {} + if vim.fn.filereadable(vim.fn.expand('~/.SpaceVim.d/tasks.toml')) == 1 then + global_conf = toml.parse_file(vim.fn.expand('~/.SpaceVim.d/tasks.toml')) + for _, v in pairs(global_conf) do + v.isGlobal = true + end + log.debug('found global conf:\n' .. vim.inspect(global_conf)) + end + if vim.fn.filereadable(vim.fn.expand('.SpaceVim.d/tasks.toml')) == 1 then + local_conf = toml.parse_file(vim.fn.expand('.SpaceVim.d/tasks.toml')) + log.debug('found local conf:\n' .. vim.inspect(local_conf)) + end + task_config = vim.fn.extend(global_conf, local_conf) +end + +local function init_variables() + variables.workspaceFolder = + file.unify_path(require('spacevim.plugin.projectmanager').current_root()) + variables.workspaceFolderBasename = vim.fn.fnamemodify(variables.workspaceFolder, ':t') + variables.file = file.unify_path(vim.fn.expand('%:p')) + variables.relativeFile = file.unify_path(vim.fn.expand('%'), ':.') + variables.relativeFileDirname = file.unify_path(vim.fn.expand('%'), ':h') + variables.fileBasename = vim.fn.expand('%:t') + variables.fileBasenameNoExtension = vim.fn.expand('%:t:r') + variables.fileDirname = file.unify_path(vim.fn.expand('%:p:h')) + variables.fileExtname = vim.fn.expand('%:e') + variables.lineNumber = vim.fn.line('.') + variables.selectedText = '' + variables.execPath = '' +end + +local function select_task(taskName) + selected_task = task_config[taskName] +end + +-- this function require menu api +local function pick() + selected_task = {} + local ques = {} + for key,_ in pairs(task_config) do + local task_name + if task_config[key].isGlobal then + task_name = key .. '(global)' + elseif task_config[key].isDetected then + task_name = task_config[key].detectedName .. key .. '(detected)' + else + task_name = key + end + table.insert(ques, {task_name, select_task, {key}}) + end + + menu.menu(ques) + return selected_task + +end + +local function replace_variables(str) + for key, _ in ipairs(variables) do + str = vim.fn.substitute(str, '${' .. key .. '}', variables[key], 'g') + end + return str +end + +local function map(t, f) + local rst = {} + for _, v in ipairs(t) do + table.insert(rst, f(v)) + end + return rst +end + +local function expand_task(task) + if task.windows and sys.isWindows then + task = task.windows + elseif task.osx and sys.isOSX then + task = task.osx + elseif task.linux and sys.isLinux then + task = task.linux + end + if task.command and type(task.command) == "string" then + task.command = replace_variables(task.command) + end + if task.args and type(task.args) == "table" then + task.args = map(task.args, replace_variables) + end + if task.options and type(task.options) == "table" then + if task.options.cwd and type(task.options.cwd) == "string" then + task.options.cwd = replace_variables(task.options.cwd) + end + end + return task +end + + +function M.edit(...) + if select(1, ...) then + vim.cmd('e ~/.SpaceVim.d/tasks.toml') + else + vim.cmd('e .SpaceVim.d/tasks.toml') + end +end + +function M.get() + load() + for _, provider in ipairs(providers) do + vim.tbl_extend(task_config, provider()) + end + init_variables() + local task = expand_task(pick()) + return task +end + + +local function open_task() + local line = vim.fn.getline('.') + local task + if string.find(line, '^%[.*%]') then + task = string.sub(vim.fn.matchstr(line, '^\\[.*\\]'), 2, -2) + vim.cmd('close') + require('spacevim.plugin.runner').run_task(expand_task(task_config[task])) + end + +end + +local function open_tasks_list_win() + if task_viewer_bufnr ~= 0 and vim.api.nvim_buf_is_valid(task_viewer_bufnr) then + vim.cmd('bd ' .. task_viewer_bufnr) + end + vim.cmd('botright split __tasks_info__') + local lines = vim.o.lines * 30 / 100 + vim.cmd('resize ' .. lines) + vim.cmd([[ + setlocal buftype=nofile bufhidden=wipe nobuflisted nolist nomodifiable + \ noswapfile + \ nowrap + \ cursorline + \ nospell + \ nonu + \ norelativenumber + \ winfixheight + \ nomodifiable + set filetype=SpaceVimTasksInfo + ]]) + task_viewer_bufnr = vim.fn.bufnr('%') + vim.api.nvim_buf_set_keymap(task_viewer_bufnr, 'n', '', '', { + callback = open_task, + }) + +end + +local function update_tasks_win_context() + local lines = {'Task Type Description'} + for task, _ in pairs(task_config) do + local line + if task_config[task].isGlobal then + line = '[' .. task .. ']' .. string.rep(' ', 22 - #task, '') .. 'global ' + elseif task_config[task].isDetected then + line = '[' .. task_config[task].detectedName .. task .. ']' .. string.rep(' ', 22 - vim.fn.strlen(task .. task_config[task].detectedName), '') .. 'detected ' + else + line = '[' .. task .. ']' .. string.rep(' ', 22 - #task, '') .. 'local ' + end + if task_config[task].description then + line = line .. task_config[task].description + else + local argv = task_config[task].args or {} + + line = line .. task_config[task].command .. ' ' .. table.concat(argv, ' ') + end + table.insert(lines, line) + end + vim.api.nvim_buf_set_option(task_viewer_bufnr, 'modifiable', true) + vim.api.nvim_buf_set_lines(task_viewer_bufnr, 0, -1, false, lines) + vim.api.nvim_buf_set_option(task_viewer_bufnr, 'modifiable', false) +end + +function M.list() + load() + for _, provider in ipairs(providers) do + vim.tbl_extend(task_config, provider()) + end + init_variables() + open_tasks_list_win() + update_tasks_win_context() +end + +function M.reg_provider(provider) + table.insert(providers, provider) +end + +function M.get_tasks() + load() + for _, provider in ipairs(providers) do + vim.tbl_extend(task_config, provider()) + end + init_variables() + return task_config +end + +return M