aboutsummaryrefslogtreecommitdiff
path: root/lua/tasks/runner.lua
diff options
context:
space:
mode:
authorHennadii Chernyshchyk <genaloner@gmail.com>2022-09-10 14:05:32 +0300
committerHennadii Chernyshchyk <genaloner@gmail.com>2022-09-10 17:48:06 +0300
commitfb10b4906aadaeff295883d171c05246943e5571 (patch)
treeeae589c4aeb613b8a2e8a1daf678f008f01069b9 /lua/tasks/runner.lua
downloadneovim-tasks-fb10b4906aadaeff295883d171c05246943e5571.tar.gz
neovim-tasks-fb10b4906aadaeff295883d171c05246943e5571.zip
Initial commit
Diffstat (limited to 'lua/tasks/runner.lua')
-rw-r--r--lua/tasks/runner.lua226
1 files changed, 226 insertions, 0 deletions
diff --git a/lua/tasks/runner.lua b/lua/tasks/runner.lua
new file mode 100644
index 0000000..21bf765
--- /dev/null
+++ b/lua/tasks/runner.lua
@@ -0,0 +1,226 @@
1local config = require('tasks.config')
2local Job = require('plenary.job')
3local runner = {}
4
5local last_job
6
7---@param lines table
8---@param errorformat string?
9local function append_to_quickfix(lines, errorformat)
10 vim.fn.setqflist({}, 'a', { efm = errorformat, lines = lines })
11 -- Scrolls the quickfix buffer if not active
12 if vim.bo.buftype ~= 'quickfix' then
13 vim.api.nvim_command('cbottom')
14 end
15end
16
17---@param errorformat? string
18---@return function: A coroutine that reads job data into quickfix.
19local function read_to_quickfix(errorformat)
20 -- Modified from https://github.com/nvim-lua/plenary.nvim/blob/968a4b9afec0c633bc369662e78f8c5db0eba249/lua/plenary/job.lua#L287
21 -- We use our own implementation to process data in chunks because
22 -- default Plenary callback processes every line which is very slow for adding to quickfix.
23 return coroutine.wrap(function(err, data, is_complete)
24 -- We repeat forever as a coroutine so that we can keep calling this.
25 local lines = {}
26 local result_index = 1
27 local result_line = nil
28 local found_newline = nil
29
30 while true do
31 if data then
32 data = data:gsub('\r', '')
33
34 local processed_index = 1
35 local data_length = #data + 1
36
37 repeat
38 local start = data:find('\n', processed_index, true) or data_length
39 local line = data:sub(processed_index, start - 1)
40 found_newline = start ~= data_length
41
42 -- Concat to last line if there was something there already.
43 -- This happens when "data" is broken into chunks and sometimes
44 -- the content is sent without any newlines.
45 if result_line then
46 result_line = result_line .. line
47
48 -- Only put in a new line when we actually have new data to split.
49 -- This is generally only false when we do end with a new line.
50 -- It prevents putting in a "" to the end of the results.
51 elseif start ~= processed_index or found_newline then
52 result_line = line
53
54 -- Otherwise, we don't need to do anything.
55 end
56
57 if found_newline then
58 if not result_line then
59 return vim.api.nvim_err_writeln('Broken data thing due to: ' .. tostring(result_line) .. ' ' .. tostring(data))
60 end
61
62 table.insert(lines, err and err or result_line)
63
64 result_index = result_index + 1
65 result_line = nil
66 end
67
68 processed_index = start + 1
69 until not found_newline
70 end
71
72 if is_complete and not found_newline then
73 table.insert(lines, err and err or result_line)
74 end
75
76 if #lines ~= 0 then
77 -- Move lines to another variable and send them to quickfix
78 local processed_lines = lines
79 lines = {}
80 vim.schedule(function() append_to_quickfix(processed_lines, errorformat) end)
81 end
82
83 if data == nil or is_complete then
84 return
85 end
86
87 err, data, is_complete = coroutine.yield()
88 end
89 end)
90end
91
92--- Run specified commands in chain.
93---@param task_name string: Name of a task to read properties.
94---@param commands table: Commands to chain.
95---@param module_config table: Module configuration.
96---@param addition_args table?: Additional arguments that will be applied to the last command.
97---@param previous_job table?: Previous job to read data from, used by this function for recursion.
98function runner.chain_commands(task_name, commands, module_config, addition_args, previous_job)
99 local command = commands[1]
100 if vim.is_callable(command) then
101 command = command(module_config, previous_job)
102 if not command then
103 return
104 end
105 end
106
107 local cwd = command.cwd or vim.loop.cwd()
108 local args = command.args and command.args or {}
109 local env = vim.tbl_extend('force', vim.loop.os_environ(), command.env and command.env or {})
110 if #commands == 1 then
111 -- Apply task parameters only to the last command
112 vim.list_extend(args, addition_args)
113 vim.list_extend(args, vim.tbl_get(module_config, 'args', task_name) or {})
114 env = vim.tbl_extend('force', env, vim.tbl_get(module_config, 'env', task_name) or {})
115 end
116
117 if command.dap_name then
118 vim.schedule(function()
119 local dap = require('dap')
120 local dap_config = dap.configurations[command.dap_name] -- Try to get an existing configuration
121 dap.run(vim.tbl_extend('force', dap_config and dap_config or { type = command.dap_name }, {
122 name = command.cmd,
123 request = 'launch',
124 program = command.cmd,
125 args = args,
126 }))
127 if config.dap_open_command then
128 vim.api.nvim_command('cclose')
129 config.dap_open_command()
130 end
131 last_job = dap
132 end)
133 return
134 end
135
136 local quickfix_output = not command.ignore_stdout and not command.ignore_stderr
137 local job = Job:new({
138 command = command.cmd,
139 args = args,
140 cwd = cwd,
141 env = env,
142 enable_recording = #commands ~= 1,
143 on_start = quickfix_output and vim.schedule_wrap(function()
144 vim.fn.setqflist({}, ' ', { title = command.cmd .. ' ' .. table.concat(args, ' ') })
145 vim.api.nvim_command(string.format('%s copen %d', config.quickfix.pos, config.quickfix.height))
146 vim.api.nvim_command('wincmd p')
147 end) or nil,
148 on_exit = vim.schedule_wrap(function(_, code, signal)
149 if quickfix_output then
150 append_to_quickfix({ 'Exited with code ' .. (signal == 0 and code or 128 + signal) })
151 end
152 if code == 0 and signal == 0 and command.after_success then
153 command.after_success()
154 end
155 end),
156 })
157
158 job:start()
159 if not command.ignore_stdout then
160 job.stdout:read_start(read_to_quickfix(command.errorformat))
161 end
162 if not command.ignore_stderr then
163 job.stderr:read_start(read_to_quickfix(command.errorformat))
164 end
165
166 if #commands ~= 1 then
167 job:after_success(function() runner.chain_commands(task_name, vim.list_slice(commands, 2), module_config, addition_args, job) end)
168 end
169 last_job = job
170end
171
172---@return string?
173function runner.get_current_job_name()
174 if not last_job then
175 return nil
176 end
177
178 -- Check if this job was run through debugger.
179 if last_job.session then
180 local session = last_job.session()
181 if not session then
182 return nil
183 end
184 return session.config.program
185 end
186
187 if last_job.is_shutdown then
188 return nil
189 end
190
191 return last_job.cmd
192end
193
194---@return boolean: `true` if a job was canceled or `false` if there is no active job.
195function runner.cancel_job()
196 if not last_job then
197 return false
198 end
199
200 -- Check if this job was run through debugger.
201 if last_job.session then
202 if not last_job.session() then
203 return false
204 end
205 last_job.terminate()
206 return true
207 end
208
209 if last_job.is_shutdown then
210 return false
211 end
212
213 last_job:shutdown(1, 9)
214
215 if vim.fn.has('win32') == 1 then
216 -- Kill all children.
217 for _, pid in ipairs(vim.api.nvim_get_proc_children(last_job.pid)) do
218 vim.loop.kill(pid, 9)
219 end
220 else
221 vim.loop.kill(last_job.pid, 9)
222 end
223 return true
224end
225
226return runner