diff options
Diffstat (limited to 'lua/tasks/runner.lua')
-rw-r--r-- | lua/tasks/runner.lua | 226 |
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 @@ | |||
1 | local config = require('tasks.config') | ||
2 | local Job = require('plenary.job') | ||
3 | local runner = {} | ||
4 | |||
5 | local last_job | ||
6 | |||
7 | ---@param lines table | ||
8 | ---@param errorformat string? | ||
9 | local 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 | ||
15 | end | ||
16 | |||
17 | ---@param errorformat? string | ||
18 | ---@return function: A coroutine that reads job data into quickfix. | ||
19 | local 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) | ||
90 | end | ||
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. | ||
98 | function 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 | ||
170 | end | ||
171 | |||
172 | ---@return string? | ||
173 | function 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 | ||
192 | end | ||
193 | |||
194 | ---@return boolean: `true` if a job was canceled or `false` if there is no active job. | ||
195 | function 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 | ||
224 | end | ||
225 | |||
226 | return runner | ||