nvim/lua/build.lua
Kenneth Benzie (Benie) f93360d854 Actually run compdb asynchronously
Turns out vim.schedule() only defers calling the function until main
event loop has some free cycles but can still block the UI, just a bit
later. This patch replaces the external command invoked in the callback
passed into vim.schedule() with vim.fn.jobstart() and moves the other
code into job callbacks.
2024-03-14 21:17:36 +00:00

173 lines
5.0 KiB
Lua

local build = {}
build.dir = nil
local function echo(message, highlight)
vim.api.nvim_echo({ { message, highlight }, }, true, {})
end
function build.set_dir(dirname)
if not dirname then
build.select_dir()
return
end
local current_dir = vim.fn.getcwd()
local build_dir = current_dir .. '/' .. dirname
-- print(build_dir)
if vim.fn.isdirectory(build_dir) == 0 then
echo('directory does not exist: ' .. build_dir, 'Error')
return
end
build.dir = build_dir
echo('Processing compile_commands.json with compdb ...', 'DiagnosticInfo')
local compile_commands = current_dir .. '/compile_commands.json'
-- Post-process compile_commands.json with compdb as an async job to avoid
-- blocking the user interface
vim.fn.jobstart(
'compdb -p ' .. build.dir .. ' list > ' .. compile_commands, {
-- Restart clangd language server
-- TODO: Configure cmake language server?
on_exit = function()
vim.cmd [[ LspRestart clangd ]]
echo('Build directory selected: ' .. dirname, 'DiagnosticInfo')
end,
-- Display any error messages to the user
on_stderr = function(_, output, _)
-- Remove any lines containing a compdb warning
local error = {}
local warning = 'WARNING:compdb'
for _, line in ipairs(output) do
if string.sub(line, 1, #warning) ~= warning then
table.insert(error, line)
end
end
-- Display the error message if there was one
if table.maxn(error) > 0 then
echo(vim.fn.join(error, '\n'), 'Error')
return
end
end,
stderr_buffered = true,
}
)
end
function build.select_dir(callback)
local dirs = build.list_dirs()
callback = callback or nil
if table.maxn(dirs) == 1 then
build.set_dir(dirs[1])
return
end
if vim.fn.exists(':Telescope') > 0 then
-- Create a Telescope picker with custom action.
require('telescope.pickers').new({}, {
prompt_title = 'Select build directory',
finder = require('telescope.finders').new_table({ results = dirs }),
sorter = require("telescope.config").values.generic_sorter({}),
attach_mappings = function(bufnr, map)
-- Override the default confirm action
map('i', '<CR>', function()
-- Get the select build directory
local state = require('telescope.actions.state')
build.set_dir(state.get_selected_entry().value)
-- Close the Telescope floating window
require('telescope.actions').close(bufnr)
-- Invoke the callback if provided
if callback then callback() end
end)
return true
end,
layout_config = {
height = function(_, _, max_lines)
-- Limit the height based on the number of dirs
return math.min(max_lines, table.maxn(dirs) + 5)
end
}
}):find()
else
-- Prompt user to choose dir with to inputlist
local choices = {}
for index, choice in ipairs(dirs) do
table.insert(choices, tostring(index) .. ': ' .. choice)
end
local index = vim.fn.inputlist(choices)
build.dir = dirs[index]
if callback then
callback()
end
end
end
function build.run(args)
-- Check if build dir set, if not prompt user to select one
if not build.dir then
build.select_dir(function() build.run(args) end)
return
end
local command = nil
if vim.fn.filereadable(build.dir .. '/build.ninja') then
command = 'ninja'
elseif vim.fn.filereadable(build.dir .. '/Makefile') then
command = 'make'
else
echo('could not find build.ninja or Makefile in: ' .. build.dir)
return
end
command = command .. ' -C ' .. build.dir .. ' ' .. vim.fn.join(args, ' ')
-- TODO: Figure out how to make the terminal window close on success
-- TODO: Running the terminal in a togglable floating window might be nice
vim.fn.execute('split | terminal ' .. command)
end
function build.list_dirs()
local dirs = vim.fn.globpath('.', 'build*')
dirs = vim.fn.substitute(dirs, '\\.\\/', '', 'g')
return vim.fn.split(dirs)
end
function build.list_targets()
local targets = {}
if not build.dir then
return targets
end
if vim.fn.filereadable(build.dir .. '/build.ninja') then
-- Query ninja for the list of targets
targets = vim.fn.systemlist('ninja -C ' .. build.dir .. ' -t targets')
for index, target in ipairs(targets) do
targets[index] = string.sub(target, 1, string.find(target, ':') - 1)
end
end
-- TODO: Support make, it's much less straight forwards than ninja.
return targets
end
function build.create_commands()
local buffer = vim.api.nvim_get_current_buf()
-- Create :BuildDir command
vim.api.nvim_buf_create_user_command(buffer, 'BuildDir', function(opts)
build.set_dir(opts.fargs[1])
end, {
bang = true, nargs = '?', complete = build.list_dirs,
})
-- Create :Build command
vim.api.nvim_buf_create_user_command(buffer, 'Build', function(opts)
build.run(opts.fargs)
end, {
bang = true, nargs = '*', complete = build.list_targets,
})
end
return build