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