Skip to content

Commit 895ed48

Browse files
author
Jose Alvarez
committed
feat(loop): implement timeout
1 parent 3f3b64c commit 895ed48

14 files changed

+300
-84
lines changed

README.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ null_ls.setup {
5050
-- define sources at setup
5151
sources = {my_sources},
5252

53-
-- options (defaults shown)
53+
-- options (defaults shown, numbers in ms)
5454
save_after_formatting = true,
55-
debounce = 250, -- ms
56-
keep_alive_interval = 60000 -- ms (60 seconds)
55+
debounce = 250,
56+
keep_alive_interval = 60000,
57+
default_timeout = 1000
5758
}
5859

5960
-- register sources dynamically

lua/null-ls/builtins/formatting.lua

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ M.lua_format = {
1111
{
1212
command = "lua-format",
1313
args = {"--single-quote-to-double-quote", "-i"},
14-
to_stdin = true
14+
to_stdin = true,
15+
timeout = 2500
1516
})
1617
}
1718

lua/null-ls/builtins/test.lua

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
local methods = require("null-ls.methods")
21
local u = require("null-ls.utils")
2+
local methods = require("null-ls.methods")
3+
local helpers = require("null-ls.helpers")
34

45
local api = vim.api
56

@@ -63,6 +64,30 @@ M.mock_code_action = {
6364
filetypes = {"lua"}
6465
}
6566

67+
M.slow_code_action = {
68+
method = methods.internal.CODE_ACTION,
69+
generator = helpers.generator_factory(
70+
{
71+
command = "bash",
72+
args = {"./test/scripts/sleep-and-echo.sh"},
73+
format = "raw",
74+
timeout = 100,
75+
on_output = function(params, done)
76+
if not params.output then return done() end
77+
78+
return done({
79+
{
80+
title = "Slow mock action",
81+
action = function()
82+
print("I took too long!")
83+
end
84+
}
85+
})
86+
end
87+
}),
88+
filetypes = {"lua"}
89+
}
90+
6691
M.mock_diagnostics = {
6792
method = methods.internal.DIAGNOSTICS,
6893
generator = {

lua/null-ls/config.lua

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ local defaults = {
66
debounce = 250,
77
keep_alive_interval = 60000, -- 60 seconds,
88
save_after_format = true,
9+
default_timeout = 1000,
910
_generators = {},
1011
_filetypes = {},
1112
_names = {},

lua/null-ls/helpers.lua

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
local u = require("null-ls.utils")
2+
local c = require("null-ls.config")
23
local loop = require("null-ls.loop")
34

45
local validate = vim.validate
@@ -51,9 +52,10 @@ local formats = {
5152

5253
M.generator_factory = function(opts)
5354
local command, args, on_output, format, to_stderr, to_stdin, ignore_errors,
54-
check_exit_code = opts.command, opts.args, opts.on_output,
55-
opts.format, opts.to_stderr, opts.to_stdin,
56-
opts.ignore_errors, opts.check_exit_code
55+
check_exit_code, timeout = opts.command, opts.args, opts.on_output,
56+
opts.format, opts.to_stderr, opts.to_stdin,
57+
opts.ignore_errors, opts.check_exit_code,
58+
opts.timeout
5759

5860
local _validated
5961
local validate_opts = function()
@@ -69,7 +71,8 @@ M.generator_factory = function(opts)
6971
to_stderr = {to_stderr, "boolean", true},
7072
to_stdin = {to_stdin, "boolean", true},
7173
ignore_errors = {ignore_errors, "boolean", true},
72-
check_exit_code = {check_exit_code, "function", true}
74+
check_exit_code = {check_exit_code, "function", true},
75+
timeout = {timeout, "number", true}
7376
})
7477

7578
_validated = true
@@ -113,7 +116,8 @@ M.generator_factory = function(opts)
113116
input = to_stdin and get_content(params) or nil,
114117
handler = wrapper,
115118
bufnr = params.bufnr,
116-
check_exit_code = check_exit_code
119+
check_exit_code = check_exit_code,
120+
timeout = timeout or c.get().default_timeout
117121
})
118122
end,
119123
filetypes = opts.filetypes,

lua/null-ls/loop.lua

+67-33
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ local u = require("null-ls.utils")
22

33
local api = vim.api
44
local uv = vim.loop
5+
local wrap = vim.schedule_wrap
56

67
local close_handle = function(handle)
78
if handle and not handle:is_closing() then handle:close() end
@@ -24,37 +25,43 @@ local parse_args = function(args, bufnr)
2425
return parsed
2526
end
2627

28+
local TIMEOUT_EXIT_CODE = 7451
29+
2730
local M = {}
2831

2932
M.spawn = function(cmd, args, opts)
30-
local handler, input, bufnr, check_exit_code = opts.handler, opts.input,
31-
opts.bufnr,
32-
opts.check_exit_code
33+
local handler, input, bufnr, check_exit_code, timeout = opts.handler,
34+
opts.input,
35+
opts.bufnr,
36+
opts.check_exit_code,
37+
opts.timeout
3338

39+
local timer
3440
local output, error_output, exit_ok = "", "", _G._TEST and true or nil
35-
local handle_stdout = vim.schedule_wrap(
36-
function(err, chunk)
37-
if err then error("stdout error: " .. err) end
38-
39-
if chunk then output = output .. chunk end
40-
if not chunk then
41-
-- wait for handler callback to check exit code
42-
vim.wait(500, function() return exit_ok ~= nil end, 10)
43-
44-
-- convert empty strings to make nil checks easier
45-
if output == "" then output = nil end
46-
if error_output == "" then error_output = nil end
47-
48-
-- if exit code is not ok and program did not output to stderr,
49-
-- assign output to error_output, so handler can process it as an error
50-
if not exit_ok and not error_output then
51-
error_output = output
52-
output = nil
53-
end
54-
55-
handler(error_output, output)
41+
local handle_stdout = wrap(function(err, chunk)
42+
if err then error("stdout error: " .. err) end
43+
44+
if chunk then output = output .. chunk end
45+
if not chunk then
46+
if timer then timer.stop(true) end
47+
48+
-- wait for handler callback to check exit code
49+
vim.wait(500, function() return exit_ok ~= nil end, 10)
50+
51+
-- convert empty strings to make nil checks easier
52+
if output == "" then output = nil end
53+
if error_output == "" then error_output = nil end
54+
55+
-- if exit code is not ok and program did not output to stderr,
56+
-- assign output to error_output, so handler can process it as an error
57+
if not exit_ok and not error_output then
58+
error_output = output
59+
output = nil
5660
end
57-
end)
61+
62+
handler(error_output, output)
63+
end
64+
end)
5865

5966
local handle_stderr = function(err, chunk)
6067
if err then error("stderr error: " .. err) end
@@ -68,9 +75,12 @@ M.spawn = function(cmd, args, opts)
6875
local stdio = {stdin, stdout, stderr}
6976

7077
local handle
71-
handle = uv.spawn(cmd, {args = parse_args(args, bufnr), stdio = stdio},
72-
vim.schedule_wrap(function(code)
73-
exit_ok = check_exit_code and check_exit_code(code) or code == 0
78+
local close = wrap(function(code)
79+
if code == TIMEOUT_EXIT_CODE then
80+
exit_ok = false
81+
else
82+
exit_ok = check_exit_code and check_exit_code(code) or code == 0
83+
end
7484

7585
stdout:read_stop()
7686
stderr:read_stop()
@@ -79,7 +89,17 @@ M.spawn = function(cmd, args, opts)
7989
close_handle(stdout)
8090
close_handle(stderr)
8191
close_handle(handle)
82-
end))
92+
end)
93+
94+
handle = uv.spawn(cmd, {args = parse_args(args, bufnr), stdio = stdio},
95+
close)
96+
if timeout then
97+
timer = M.timer(timeout, nil, true, function()
98+
close(TIMEOUT_EXIT_CODE)
99+
handler()
100+
timer.stop(true)
101+
end)
102+
end
83103

84104
uv.read_start(stdout, handle_stdout)
85105
uv.read_start(stderr, handle_stderr)
@@ -91,18 +111,32 @@ M.timer = function(timeout, interval, should_start, callback)
91111
interval = interval or 0
92112

93113
local timer = uv.new_timer()
94-
local wrapped = vim.schedule_wrap(callback)
114+
local wrapped = wrap(callback)
115+
95116
local start = function() timer:start(timeout, interval, wrapped) end
96-
local stop = function() timer:stop() end
117+
local close = function() close_handle(timer) end
118+
local stop = function(should_close)
119+
timer:stop()
120+
if should_close then close() end
121+
end
97122
local restart = function(new_timeout, new_interval)
98123
timer:stop()
99124
timer:start(new_timeout or timeout, new_interval or interval, wrapped)
100125
end
101126

102127
if should_start then timer:start(timeout, interval, wrapped) end
103-
return {_timer = timer, start = start, stop = stop, restart = restart}
128+
return {
129+
_timer = timer,
130+
start = start,
131+
stop = stop,
132+
restart = restart,
133+
close = close
134+
}
104135
end
105136

106-
if _G._TEST then M._parse_args = parse_args end
137+
if _G._TEST then
138+
M._parse_args = parse_args
139+
M._TIMEOUT_EXIT_CODE = TIMEOUT_EXIT_CODE
140+
end
107141

108142
return M

lua/null-ls/server.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ M.start = function()
2323
-- start timer to shutdown server after inactivity
2424
local timer
2525
local shutdown = function()
26-
if timer then timer.stop() end
26+
if timer then timer.stop(true) end
2727
vim.cmd("noautocmd qa!")
2828
end
2929
timer = loop.timer(default_shutdown_timeout, nil, true, shutdown)

lua/null-ls/state.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ end
4141

4242
M.shutdown_client = function(timeout)
4343
if not state.client then return end
44-
if state.keep_alive_timer then state.keep_alive_timer.stop() end
44+
if state.keep_alive_timer then state.keep_alive_timer.stop(true) end
4545

4646
lsp.stop_client(state.client_id)
4747
vim.wait(timeout or 5000, function()

test/scripts/sleep-and-echo.sh

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
sleep 0.25
2+
echo "done"

test/spec/e2e_spec.lua

+11
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ describe("e2e", function()
7474

7575
assert.equals(vim.tbl_count(actions[1].result), 2)
7676
end)
77+
78+
it("should handle code action timeout", function()
79+
-- action calls a script that waits for 250 ms,
80+
-- but action timeout is 100 ms
81+
c.register(builtins._test.slow_code_action)
82+
83+
actions = lsp.buf_request_sync(api.nvim_get_current_buf(),
84+
methods.lsp.CODE_ACTION)
85+
86+
assert.equals(vim.tbl_count(actions[1].result), 1)
87+
end)
7788
end)
7889

7990
describe("diagnostics", function()

test/spec/helpers_spec.lua

+20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
local stub = require("luassert.stub")
22
local loop = require("null-ls.loop")
33

4+
local c = require("null-ls.config")
45
local test_utils = require("test.utils")
56

67
describe("helpers", function()
@@ -147,6 +148,25 @@ describe("helpers", function()
147148
assert.same(loop.spawn.calls[1].refs[2], {})
148149
end)
149150

151+
it("should call loop.spawn with specified timeout", function()
152+
generator_args.timeout = 500
153+
local generator = helpers.generator_factory(generator_args)
154+
155+
generator.fn({})
156+
157+
assert.same(loop.spawn.calls[1].refs[3].timeout, 500)
158+
end)
159+
160+
it("should call loop.spawn with default timeout", function()
161+
generator_args.timeout = nil
162+
local generator = helpers.generator_factory(generator_args)
163+
164+
generator.fn({})
165+
166+
assert.same(loop.spawn.calls[1].refs[3].timeout,
167+
c.get().default_timeout)
168+
end)
169+
150170
it(
151171
"should call loop.spawn with buffer content as string when to_stdin = true",
152172
function()

0 commit comments

Comments
 (0)