--============================================================================= -- job.lua --- -- Copyright (c) 2016-2022 Wang Shidong & Contributors -- Author: Wang Shidong < wsdjeg@outlook.com > -- URL: https://spacevim.org -- License: GPLv3 --============================================================================= local M = {} local uv = vim.loop local _jobs = {} local _jobid = 0 local function buffered_data(eof, data) data = data:gsub('\r', '') local std_data = vim.split(data, '\n') if #std_data > 1 and std_data[#std_data] == '' then std_data[1] = eof .. std_data[1] table.remove(std_data, #std_data) eof = '' elseif #std_data > 1 then std_data[1] = eof .. std_data[1] eof = std_data[#std_data] table.remove(std_data, #std_data) elseif #std_data == 1 and std_data[1] == '' and eof ~= '' then std_data = { eof } eof = '' elseif #std_data == 1 and std_data[#std_data] ~= '' then eof = std_data[#std_data] end return eof, std_data end local function new_job_obj(id, handle, opt, state) local jobobj = { id = id, handle = handle, opt = opt, state = state, } return jobobj end local function default_dev() -- {{{ local env = vim.fn.environ() env['NVIM'] = vim.v.servername env['NVIM_LISTEN_ADDRESS'] = nil env['NVIM_LOG_FILE'] = nil env['VIMRUNTIME'] = nil return env end -- }}} local function setup_env(env, clear_env) -- {{{ if clear_env then return env end --- @type table env = vim.tbl_extend('force', default_dev(), env or {}) local renv = {} --- @type string[] for k, v in pairs(env) do renv[#renv + 1] = string.format('%s=%s', k, tostring(v)) end return renv end -- }}} --- @param cmd string|table Spawns {cmd} as a job. --- @param opts table job options --- @return integer # jobid if job run successfully. --- jobid: if job run successfully --- 0: if type of cmd is wrong --- -1: if cmd[1] is not executable function M.start(cmd, opts) local command = '' local argv = {} if type(cmd) == 'string' then if cmd == '' then return 0 end local shell = vim.fn.split(vim.o.shell) local shellcmdflag = vim.fn.split(vim.o.shellcmdflag) -- :call jobstart(split(&shell) + split(&shellcmdflag) + ['{cmd}']) command = shell[1] argv = vim.list_slice(shell, 2) for _, v in ipairs(shellcmdflag) do table.insert(argv, v) end table.insert(argv, cmd) elseif type(cmd) == 'table' then if #cmd == 0 then return 0 end for _, v in ipairs(cmd) do if type(v) ~= 'string' then return 0 end end command = cmd[1] if command == '' then return 0 end if vim.fn.executable(command) == 0 then return -1 end argv = vim.list_slice(cmd, 2) else return 0 end local stdin = uv.new_pipe() local stdout = uv.new_pipe() local stderr = uv.new_pipe() local opt = { stdio = { stdin, stdout, stderr }, args = argv, cwd = opts.cwd or nil, hide = true, detached = opts.detached or nil, env = setup_env(opts.env, opts.clear_env), } _jobid = _jobid + 1 local current_id = _jobid local exit_cb if opts.on_exit then exit_cb = function(code, singin) if stdout and not stdout:is_closing() then stdout:close() end if stderr and not stderr:is_closing() then stderr:close() end if stdin and not stdin:is_closing() then stdin:close() end local job = _jobs['jobid_' .. current_id] if job and job.handle and not job.handle:is_closing() then job.handle:close() end vim.schedule(function() opts.on_exit(current_id, code, singin) end) end else exit_cb = function(code, singin) if stdout and not stdout:is_closing() then stdout:close() end if stderr and not stderr:is_closing() then stderr:close() end if stdin and not stdin:is_closing() then stdin:close() end local job = _jobs['jobid_' .. current_id] if job and job.handle and not job.handle:is_closing() then job.handle:close() end end end local handle, pid = uv.spawn(command, opt, exit_cb) _jobs['jobid_' .. _jobid] = new_job_obj(_jobid, handle, opts, { stdout = stdout, stderr = stderr, stdin = stdin, pid = pid, stderr_eof = '', stdout_eof = '', }) -- logger.debug(vim.inspect(_jobs['jobid_' .. _jobid])) if opts.on_stdout then -- define on_stdout function based on stdout's nparams local nparams = debug.getinfo(opts.on_stdout).nparams if nparams == 2 then uv.read_start(stdout, function(err, data) if data then local stdout_data _jobs['jobid_' .. current_id].state.stdout_eof, stdout_data = buffered_data(_jobs['jobid_' .. current_id].state.stdout_eof, data) vim.schedule(function() opts.on_stdout(current_id, stdout_data) end) end end) else uv.read_start(stdout, function(err, data) if data then local stdout_data _jobs['jobid_' .. current_id].state.stdout_eof, stdout_data = buffered_data(_jobs['jobid_' .. current_id].state.stdout_eof, data) vim.schedule(function() opts.on_stdout(current_id, stdout_data, 'stdout') end) end end) end end if opts.on_stderr then local nparams = debug.getinfo(opts.on_stderr).nparams if nparams == 2 then uv.read_start(stderr, function(err, data) if data then local stderr_data _jobs['jobid_' .. current_id].state.stderr_eof, stderr_data = buffered_data(_jobs['jobid_' .. current_id].state.stderr_eof, data) vim.schedule(function() opts.on_stderr(current_id, stderr_data) end) end end) else uv.read_start(stderr, function(err, data) if data then local stderr_data _jobs['jobid_' .. current_id].state.stderr_eof, stderr_data = buffered_data(_jobs['jobid_' .. current_id].state.stderr_eof, data) vim.schedule(function() opts.on_stderr(current_id, stderr_data, 'stderr') end) end end) end end return current_id end function M.send(id, data) -- {{{ local jobobj = _jobs['jobid_' .. id] if not jobobj then error('can not find job:' .. id) end local stdin = jobobj.state.stdin if not stdin then error('no stdin stream for jobid:' .. id) end if type(data) == 'table' then for _, v in ipairs(data) do stdin:write(v) stdin:write('\n') end elseif type(data) == 'string' then stdin:write(data) stdin:write('\n') elseif data == nil then stdin:write('', function() stdin:shutdown(function() if stdin then stdin:close() end end) end) end end function M.chanclose(id, t) local jobobj = _jobs['jobid_' .. id] if not jobobj then error('can not find job:' .. id) end if t == 'stdin' then local stdin = jobobj.state.stdin if stdin and not stdin:is_closing() then stdin:close() end elseif t == 'stdout' then local stdout = jobobj.state.stdout if stdout and not stdout:is_closing() then stdout:close() end elseif t == 'stderr' then local stderr = jobobj.state.stderr if stderr and not stderr:is_closing() then stderr:close() end else error('the type only can be:stdout, stdin or stderr') end end function M.stop(id) local jobobj = _jobs['jobid_' .. id] if not jobobj then return end local handle = jobobj.handle handle:kill(6) end return M