From fb10b4906aadaeff295883d171c05246943e5571 Mon Sep 17 00:00:00 2001 From: Hennadii Chernyshchyk Date: Sat, 10 Sep 2022 14:05:32 +0300 Subject: Initial commit --- lua/tasks/module/cargo.lua | 171 ++++++++++++++++++++++++++++ lua/tasks/module/cmake.lua | 272 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 443 insertions(+) create mode 100644 lua/tasks/module/cargo.lua create mode 100644 lua/tasks/module/cmake.lua (limited to 'lua/tasks/module') diff --git a/lua/tasks/module/cargo.lua b/lua/tasks/module/cargo.lua new file mode 100644 index 0000000..3e21371 --- /dev/null +++ b/lua/tasks/module/cargo.lua @@ -0,0 +1,171 @@ +local utils = require('tasks.utils') +local Job = require('plenary.job') +local Path = require('plenary.path') +local cargo = {} + +-- Modified version of `errorformat` from the official Rust plugin for Vim: +-- https://github.com/rust-lang/rust.vim/blob/4aa69b84c8a58fcec6b6dad6fe244b916b1cf830/compiler/rustc.vim#L32 +-- https://github.com/rust-lang/rust.vim/blob/4aa69b84c8a58fcec6b6dad6fe244b916b1cf830/compiler/cargo.vim#L35 +-- We display all lines (not only error messages) since we show output in quickfix. +-- Zero-width look-ahead regex is used to avoid marking general messages as errors: %\%%(ignored text%\)%\@!. +local errorformat = [[%Eerror: %\%%(aborting %\|could not compile%\)%\@!%m,]] + .. [[%Eerror[E%n]: %m,]] + .. [[%Inote: %m,]] + .. [[%Wwarning: %\%%(%.%# warning%\)%\@!%m,]] + .. [[%C %#--> %f:%l:%c,]] + .. [[%E left:%m,%C right:%m %f:%l:%c,%Z,]] + .. [[%.%#panicked at \'%m\'\, %f:%l:%c]] + +--- Detects package name from command line arguments. +---@param args table +---@return string? +local function detect_package_name(args) + for index, value in ipairs(args) do + if value == '-p' or value == '--package' or value == '--bin' then + return args[index + 1] + end + end + return nil +end + +--- Returns only a packages that can be executed. +---@param packages table: Packages to filter. +---@return table +local function find_executable_packages(packages) + local executables = {} + for _, line in pairs(packages) do + local package = vim.json.decode(line) + if package.executable and package.executable ~= vim.NIL then + table.insert(executables, package) + end + end + return executables +end + +--- Finds executable package name from a list of packages. +---@param packages table +---@param args table?: Command line arguments that will be used to detect an executable if JSON message from cargo is missing this info. +---@return table? +local function get_executable_package(packages, args) + local executable_packages = find_executable_packages(packages) + if #executable_packages == 1 then + return executable_packages[1] + end + + -- Try to detect package name from arguments + local package_name = detect_package_name(args or {}) + if not package_name then + local available_names = {} + for _, package in ipairs(executable_packages) do + table.insert(available_names, package.target.name) + end + utils.notify( + 'Could not determine which binary to run\nUse the "--bin" or "--package" option to specify a binary\nAvailable binaries: ' .. table.concat(available_names, ', '), + vim.log.levels.ERROR + ) + return nil + end + + for _, package in ipairs(executable_packages) do + if package.target.name == package_name then + return package + end + end + + utils.notify(string.format('Unable to find package named "%s"', package_name), vim.log.levels.ERROR) + return nil +end + +---@return table: List of functions for each cargo subcommand that return a task table. +local function get_cargo_subcommands() + local cargo_subcommands = {} + + local job = Job:new({ + command = 'cargo', + args = { '--list' }, + enabled_recording = true, + }) + job:sync() + + if job.code ~= 0 or job.signal ~= 0 then + utils.notify('Unable to get list of available cargo subcommands', vim.log.levels.ERROR) + return {} + end + + local start_offset = 5 + for index, line in ipairs(job:result()) do + if index ~= 1 and not line:find('alias:') then + local subcommand_end = line:find(' ', start_offset) + local subcommand = line:sub(start_offset, subcommand_end and subcommand_end - 1 or nil) + cargo_subcommands[subcommand] = + function(module_config, _) return { cmd = 'cargo', args = vim.list_extend({ subcommand }, utils.split_args(module_config.global_cargo_args)), errorformat = errorformat } end + end + end + + return cargo_subcommands +end + +--- Task +---@return table? +local function build_test(module_config, _) + return { + cmd = 'cargo', + args = vim.list_extend({ 'test', '--no-run', '--message-format=json' }, utils.split_args(module_config.global_cargo_args)), + errorformat = errorformat, + ignore_stdout = true, + } +end + +--- Task +---@param module_config table +---@param previous_job table +---@return table? +local function debug_test(module_config, previous_job) + local package = get_executable_package(previous_job:result(), vim.tbl_get(module_config, 'args', 'debug_test')) + if not package then + return + end + + return { + cmd = package.executable, + dap_name = module_config.dap_name, + errorformat = errorformat, + } +end + +--- Task +---@param module_config table +---@return table? +local function build(module_config, _) + return { + cmd = 'cargo', + args = vim.list_extend({ 'build', '--message-format=json' }, utils.split_args(module_config.global_cargo_args)), + ignore_stdout = true, + } +end + +--- Task +---@param module_config table +---@param previous_job table +---@return table? +local function debug(module_config, previous_job) + local package = get_executable_package(previous_job:result(), vim.tbl_get(module_config, 'args', 'debug')) + if not package then + return + end + + return { + cmd = package.executable, + dap_name = module_config.dap_name, + errorformat = errorformat, + } +end + +cargo.params = { + 'dap_name', + 'global_cargo_args', +} +cargo.condition = function() return Path:new('Cargo.toml'):exists() end +cargo.tasks = vim.tbl_extend('force', get_cargo_subcommands(), { debug_test = { build_test, debug_test }, debug = { build, debug } }) + +return cargo diff --git a/lua/tasks/module/cmake.lua b/lua/tasks/module/cmake.lua new file mode 100644 index 0000000..4900aec --- /dev/null +++ b/lua/tasks/module/cmake.lua @@ -0,0 +1,272 @@ +local Path = require('plenary.path') +local utils = require('tasks.utils') +local scandir = require('plenary.scandir') +local ProjectConfig = require('tasks.project_config') +local os = require('ffi').os:lower() +local cmake = {} + +--- Parses build dir expression. +---@param dir string: Path with expressions to replace. +---@param build_type string +---@return table +local function parse_dir(dir, build_type) + local parsed_dir = dir:gsub('{cwd}', vim.loop.cwd()) + parsed_dir = parsed_dir:gsub('{os}', os) + parsed_dir = parsed_dir:gsub('{build_type}', build_type:lower()) + return Path:new(parsed_dir) +end + +--- Returns reply directory that contains targets information. +---@param build_dir table +---@return unknown +local function get_reply_dir(build_dir) return build_dir / '.cmake' / 'api' / 'v1' / 'reply' end + +--- Reads information about target. +---@param codemodel_target table +---@param reply_dir table +---@return table +local function get_target_info(codemodel_target, reply_dir) return vim.json.decode((reply_dir / codemodel_target['jsonFile']):read()) end + +--- Creates query files that to acess information about targets after CMake configuration. +---@param build_dir table +---@return boolean: Returns `true` on success. +local function make_query_files(build_dir) + local query_dir = build_dir / '.cmake' / 'api' / 'v1' / 'query' + if not query_dir:mkdir({ parents = true }) then + utils.notify(string.format('Unable to create "%s"', query_dir.filename), vim.log.levels.ERROR) + return false + end + + local codemodel_file = query_dir / 'codemodel-v2' + if not codemodel_file:is_file() then + if not codemodel_file:touch() then + utils.notify(string.format('Unable to create "%s"', codemodel_file.filename), vim.log.levels.ERROR) + return false + end + end + return true +end + +--- Reads targets information. +---@param reply_dir table +---@return table? +local function get_codemodel_targets(reply_dir) + local found_files = scandir.scan_dir(reply_dir.filename, { search_pattern = 'codemodel*' }) + if #found_files == 0 then + utils.notify('Unable to find codemodel file', vim.log.levels.ERROR) + return nil + end + local codemodel = Path:new(found_files[1]) + local codemodel_json = vim.json.decode(codemodel:read()) + return codemodel_json['configurations'][1]['targets'] +end + +---@return table? +local function get_target_names() + local project_config = ProjectConfig.new() + local build_dir = parse_dir(project_config.cmake.build_dir, project_config.cmake.build_type) + if not build_dir:is_dir() then + utils.notify(string.format('Build directory "%s" does not exist, you need to run "configure" task first', build_dir), vim.log.levels.ERROR) + return nil + end + + local reply_dir = get_reply_dir(build_dir) + local codemodel_targets = get_codemodel_targets(reply_dir) + if not codemodel_targets then + return nil + end + + local targets = {} + for _, target in ipairs(codemodel_targets) do + local target_info = get_target_info(target, reply_dir) + local target_name = target_info['name'] + if target_name:find('_autogen') == nil then + table.insert(targets, target_name) + end + end + + return targets +end + +--- Finds path to an executable. +---@param build_dir table +---@param name string +---@param reply_dir table +---@return unknown? +local function get_executable_path(build_dir, name, reply_dir) + for _, target in ipairs(get_codemodel_targets(reply_dir)) do + if name == target['name'] then + local target_info = get_target_info(target, reply_dir) + if target_info['type'] ~= 'EXECUTABLE' then + utils.notify(string.format('Specified target "%s" is not an executable', name), vim.log.levels.ERROR) + return nil + end + + local target_path = Path:new(target_info['artifacts'][1]['path']) + if not target_path:is_absolute() then + target_path = build_dir / target_path + end + + return target_path + end + end + + utils.notify(string.format('Unable to find target named "%s"', name), vim.log.levels.ERROR) + return nil +end + +--- Copies compile_commands.json file from build directory to the current working directory for LSP integration. +local function copy_compile_commands() + local project_config = ProjectConfig.new() + local filename = 'compile_commands.json' + local source = parse_dir(project_config.cmake.build_dir, project_config.cmake.build_type) / filename + local destination = Path:new(vim.loop.cwd(), filename) + source:copy({ destination = destination.filename }) +end + +--- Task +---@param module_config table +---@return table? +local function configure(module_config, _) + local build_dir = parse_dir(module_config.build_dir, module_config.build_type) + build_dir:mkdir({ parents = true }) + if not make_query_files(build_dir) then + return nil + end + + return { + cmd = module_config.cmd, + args = { '-B', build_dir.filename, '-D', 'CMAKE_BUILD_TYPE=' .. module_config.build_type }, + after_success = copy_compile_commands, + } +end + +--- Task +---@param module_config table +---@return table +local function build(module_config, _) + local build_dir = parse_dir(module_config.build_dir, module_config.build_type) + + local args = { '--build', build_dir.filename } + if module_config.target then + vim.list_extend(args, { '--target', module_config.target }) + end + + return { + cmd = module_config.cmd, + args = args, + after_success = copy_compile_commands, + } +end + +--- Task +---@param module_config table +---@return table +local function build_all(module_config, _) + local build_dir = parse_dir(module_config.build_dir, module_config.build_type) + + return { + cmd = module_config.cmd, + args = { '--build', build_dir.filename }, + after_success = copy_compile_commands, + } +end + +--- Task +---@param module_config table +---@return table? +local function run(module_config, _) + if not module_config.target then + utils.notify('No selected target, please set "target" parameter', vim.log.levels.ERROR) + return nil + end + + local build_dir = parse_dir(module_config.build_dir, module_config.build_type) + if not build_dir:is_dir() then + utils.notify(string.format('Build directory "%s" does not exist, you need to run "configure" task first', build_dir), vim.log.levels.ERROR) + return nil + end + + local target_path = get_executable_path(build_dir, module_config.target, get_reply_dir(build_dir)) + if not target_path then + return + end + + if not target_path:is_file() then + utils.notify(string.format('Selected target "%s" is not built', target_path.filename), vim.log.levels.ERROR) + return nil + end + + return { + cmd = target_path.filename, + cwd = target_path:parent().filename, + } +end + +--- Task +---@param module_config table +---@return table? +local function debug(module_config, _) + if module_config.build_type ~= 'Debug' and module_config.build_type ~= 'RelWithDebInfo' then + utils.notify( + string.format('For debugging your "build_type" param should be set to "Debug" or "RelWithDebInfo", but your current build type is "%s"'), + module_config.build_type, + vim.log.levels.ERROR + ) + return nil + end + + local command = run(module_config, nil) + if not command then + return nil + end + + command.dap_name = module_config.dap_name + return command +end + +--- Task +---@param module_config table +---@return table +local function clean(module_config, _) + local build_dir = parse_dir(module_config.build_dir, module_config.build_type) + + return { + cmd = module_config.cmd, + args = { '--build', build_dir.filename, '--target', 'clean' }, + after_success = copy_compile_commands, + } +end + +--- Task +---@param module_config table +---@return table +local function open_build_dir(module_config, _) + local build_dir = parse_dir(module_config.build_dir, module_config.build_type) + + return { + cmd = os == 'windows' and 'start' or 'xdg-open', + args = { build_dir.filename }, + ignore_stdout = true, + ignore_stderr = true, + } +end + +cmake.params = { + target = get_target_names, + build_type = { 'Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel' }, + 'cmake', + 'dap_name', +} +cmake.condition = function() return Path:new('CMakeLists.txt'):exists() end +cmake.tasks = { + configure = configure, + build = build, + build_all = build_all, + run = { build, run }, + debug = debug, + clean = clean, + open_build_dir = open_build_dir, +} + +return cmake -- cgit v1.2.3