Skip to content

Commit 40dc2e9

Browse files
authored
helpers: run dynamic_command immediately when opening a buffer (#220)
1 parent 2d7ed80 commit 40dc2e9

File tree

7 files changed

+110
-15
lines changed

7 files changed

+110
-15
lines changed

doc/HELPERS.md

+12-4
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,17 @@ directory defaults to the project's root.
106106
### dynamic_command
107107

108108
Optional callback to set `command` dynamically. Takes two arguments, a `params`
109-
table and a `done` callback. The generator's original command (if set) is
110-
available as `params.command`. The `done` callback should be invoked with a
111-
string containing the command to run or `nil`, meaning that no command should
112-
run.
109+
table and a `done` callback.
110+
111+
`params` is a table with the following keys:
112+
113+
- `bufnr`
114+
- `bufname`
115+
- `root`
116+
- `command`: The generator's original command (if set)
117+
118+
The `done` callback should be invoked with a string containing the command to
119+
run or `nil`, meaning that no command should run.
113120

114121
`dynamic_command` runs every time its parent generator runs and can affect
115122
performance, so it's best to cache its output when possible.
@@ -281,6 +288,7 @@ helpers.make_builtin({
281288
factory, -- function (optional)
282289
filetypes, -- table
283290
generator, -- function (optional, but required if factory is not set)
291+
setup_buffer_async, -- function (optional)
284292
generator_opts, -- table
285293
method, -- internal null-ls method (string)
286294
meta, -- table
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
local h = require("null-ls.helpers")
2+
local methods = require("null-ls.methods")
3+
4+
local CODE_ACTION = methods.internal.CODE_ACTION
5+
6+
return h.make_builtin({
7+
method = CODE_ACTION,
8+
filetypes = { "text" },
9+
generator_opts = {
10+
on_output = function(_params, done)
11+
return done({
12+
{
13+
title = "an action",
14+
action = function() end,
15+
},
16+
})
17+
end,
18+
},
19+
factory = function(opts)
20+
opts._dynamic_command_call_count = 0
21+
opts.dynamic_command = function(_params, done)
22+
opts._dynamic_command_call_count = opts._dynamic_command_call_count + 1
23+
done("ls")
24+
end
25+
return h.generator_factory(opts)
26+
end,
27+
})

lua/null-ls/client.lua

+11
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,17 @@ M.setup_buffer = function(bufnr)
173173
return
174174
end
175175

176+
-- Notify each generator for this filetype. This gives them a chance to
177+
-- precompute information.
178+
local filetype = api.nvim_get_option_value("filetype", { buf = bufnr })
179+
for _, source in ipairs(require("null-ls.sources").get({ filetype = filetype })) do
180+
if source.generator.setup_buffer_async then
181+
source.generator.setup_buffer_async(bufnr, function()
182+
-- Nothing to do here.
183+
end)
184+
end
185+
end
186+
176187
local on_attach = c.get().on_attach
177188
if on_attach then
178189
on_attach(client, bufnr)

lua/null-ls/helpers/cache.lua

+6-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ end
88

99
M._reset()
1010

11+
---@class NullLsCacheParams
12+
---@field bufnr number
13+
---@field root string
14+
1115
--- creates a function that caches the output of a callback, indexed by bufnr
1216
---@param cb function
13-
---@return fun(params: NullLsParams): any
17+
---@return fun(params: NullLsCacheParams): any
1418
M.by_bufnr = function(cb)
1519
-- assign next available key, since we just want to avoid collisions
1620
local key = next_key
@@ -35,7 +39,7 @@ end
3539

3640
--- creates a function that caches the output of an async callback, indexed by bufnr
3741
---@param cb function
38-
---@return fun(params: NullLsParams): any
42+
---@return fun(params: NullLsCacheParams): any
3943
M.by_bufnr_async = function(cb)
4044
-- assign next available key, since we just want to avoid collisions
4145
local key = next_key

lua/null-ls/helpers/command_resolver.lua

+1-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ end
2929
--- creates a resolver that searches for a local executable and caches results by bufnr
3030
---@param prefix string|nil
3131
M.generic = function(prefix)
32-
---@param params NullLsParams
33-
---@return string|nil
32+
---@param params NullLsDynamicCommandParams
3433
return cache.by_bufnr_async(function(params, done)
3534
local executable_to_find = prefix and u.path.join(prefix, params.command) or params.command
3635
if not executable_to_find then

lua/null-ls/helpers/generator_factory.lua

+27-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ local log = require("null-ls.logger")
33
local s = require("null-ls.state")
44
local u = require("null-ls.utils")
55

6+
---@class NullLsDynamicCommandParams
7+
---@field bufnr number
8+
---@field root string
9+
---@field bufname string
10+
---@field command string?
11+
612
local output_formats = {
713
raw = "raw", -- receive error_output and output directly
814
none = nil, -- same as raw but will not send error output
@@ -133,12 +139,6 @@ return function(opts)
133139
end
134140
end
135141

136-
if dynamic_command == nil then
137-
dynamic_command = function(params, done)
138-
done(params.command)
139-
end
140-
end
141-
142142
local is_nil_table_or_func = function(v)
143143
return v == nil or vim.tbl_contains({ "function", "table" }, type(v))
144144
end
@@ -184,7 +184,27 @@ return function(opts)
184184
return true
185185
end
186186

187+
local function get_command_async(bufnr, done)
188+
if dynamic_command then
189+
dynamic_command({
190+
bufnr = bufnr,
191+
bufname = vim.api.nvim_buf_get_name(0),
192+
root = u.get_root(),
193+
command = command,
194+
}, done)
195+
else
196+
done(command)
197+
end
198+
end
199+
187200
return {
201+
setup_buffer_async = function(bufnr, done)
202+
-- Here we invoke `get_command_async` and throw away the result. Why? If
203+
-- the underlying `dynamic_command` is expensive to compute, it's
204+
-- nice to immediately start computing it, rather than waiting for
205+
-- someone to need it.
206+
get_command_async(bufnr, done)
207+
end,
188208
fn = function(params, done)
189209
local loop = require("null-ls.loop")
190210

@@ -285,7 +305,7 @@ return function(opts)
285305

286306
params.command = command
287307

288-
dynamic_command(params, function(resolved_command)
308+
get_command_async(params.bufnr, function(resolved_command)
289309
-- if dynamic_command returns nil, don't fall back to command
290310
if not resolved_command then
291311
log:debug(string.format("unable to resolve command %s; aborting", command))

test/spec/e2e_spec.lua

+26
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,32 @@ describe("e2e", function()
612612
end)
613613
end)
614614

615+
describe("dynamic_command", function()
616+
it("should run immediately, plus each time we need a command", function()
617+
local source = builtins._test.dynamic_command_code_action
618+
sources.register(source)
619+
tu.edit_test_file("test-file.txt")
620+
tu.wait()
621+
622+
-- Make sure we already ran `dynamic_command` once, even though we haven't needed it yet.
623+
-- This allows generators to pre-compute `dynamic_command` if it's
624+
-- time consuming to compute.
625+
assert.equals(1, source._opts._dynamic_command_call_count)
626+
627+
-- Query for code actions, make sure we've run `dynamic_command` twice.
628+
local actions = get_code_actions()
629+
tu.wait()
630+
assert.equals("ls", source._opts._last_command)
631+
assert.equals(2, source._opts._dynamic_command_call_count)
632+
633+
-- Query for code actions, make sure we've run `dynamic_command` thrice.
634+
local actions = get_code_actions()
635+
tu.wait()
636+
assert.equals("ls", source._opts._last_command)
637+
assert.equals(3, source._opts._dynamic_command_call_count)
638+
end)
639+
end)
640+
615641
-- https://github.com/neovim/neovim/commit/8260e4860b27a54a061bd8e2a9da23069993953a
616642
-- hover no longer supports handler
617643
if not vim.fn.has("nvim-0.11") == 1 then

0 commit comments

Comments
 (0)