From 6184ca819279c7e44f470a731fcca3d469f40a01 Mon Sep 17 00:00:00 2001 From: Daniil Rozanov Date: Mon, 18 Mar 2024 01:49:23 +0300 Subject: feat: new CMakeConfigureLast and CMakeConfigureDir commands. CMakeConfigure and CMakeConfigureDir now have vim.ui.* interface to edit --- lua/cmake-explorer.lua | 118 ++++++++++++++++++++++---------- lua/cmake-explorer/build.lua | 107 ----------------------------- lua/cmake-explorer/build_list.lua | 57 ++++++++++++++++ lua/cmake-explorer/capabilities.lua | 42 ++++++++++++ lua/cmake-explorer/config.lua | 5 +- lua/cmake-explorer/constants.lua | 7 -- lua/cmake-explorer/file_api.lua | 93 +++++++++++++++++++++++++ lua/cmake-explorer/globals.lua | 29 -------- lua/cmake-explorer/notification.lua | 13 ++++ lua/cmake-explorer/project.lua | 133 +++++++++++++++--------------------- lua/cmake-explorer/runner.lua | 5 ++ lua/cmake-explorer/utils.lua | 103 +++++++++++++++++++++++++--- 12 files changed, 440 insertions(+), 272 deletions(-) delete mode 100644 lua/cmake-explorer/build.lua create mode 100644 lua/cmake-explorer/build_list.lua create mode 100644 lua/cmake-explorer/capabilities.lua delete mode 100644 lua/cmake-explorer/constants.lua create mode 100644 lua/cmake-explorer/file_api.lua delete mode 100644 lua/cmake-explorer/globals.lua create mode 100644 lua/cmake-explorer/notification.lua diff --git a/lua/cmake-explorer.lua b/lua/cmake-explorer.lua index 8e01a57..eec4f5d 100644 --- a/lua/cmake-explorer.lua +++ b/lua/cmake-explorer.lua @@ -1,68 +1,112 @@ -local globals = require("cmake-explorer.globals") local config = require("cmake-explorer.config") local runner = require("cmake-explorer.runner") local Project = require("cmake-explorer.project") -local Build = require("cmake-explorer.build") +local capabilities = require("cmake-explorer.capabilities") +local utils = require("cmake-explorer.utils") +local Path = require("plenary.path") local M = {} -local projects = {} +local project = nil -local current_project = nil - -local function set_current_project(path) - if path then - for _, v in ipairs(projects) do - -- print(v.path:absolute() .. " ? " .. path) - if v.path:absolute() == path then - current_project = v - return - end +local format_build_dir = function() + if Path:new(config.build_dir):is_absolute() then + return function(v) + return Path:new(v.path):make_relative(vim.env.HOME) end - end - if #projects ~= 0 then - current_project = projects[1] else - print("set_current_project. no projects available") + return function(v) + return Path:new(v.path):make_relative(project.path) + end end end function M.list_build_dirs() - if current_project then - vim.print(current_project:list_build_dirs_names()) + if project then + vim.print(project:list_build_dirs()) end end -function M.configure(opts) - print("configure. #projects " .. #projects) - if current_project then - runner.start(current_project:configure(opts)) - end +function M.configure() + assert(project) + local generators = capabilities.generators() + table.insert(generators, 1, "Default") + vim.ui.select(generators, { prompt = "Select generator" }, function(generator) + if not generator then + return + end + -- TODO: handle default generator from env (or from anywhere else) + generator = utils.is_neq(generator, "Default") + vim.ui.select(config.build_types, { prompt = "Select build type" }, function(build_type) + if not build_type then + return + end + vim.ui.input({ prompt = "Input additional args" }, function(args) + if not build_type then + return + end + local task = project:configure({ generator = generator, build_type = build_type, args = args }) + runner.start(task) + end) + end) + end) +end + +function M.configure_dir() + assert(project) + + vim.ui.select( + project:list_build_dirs(), + { prompt = "Select directory to build", format_item = format_build_dir() }, + function(dir) + if not dir then + return + end + local task = project:configure(dir.path) + runner.start(task) + end + ) +end + +function M.configure_last() + local task = project:configure_last() + runner.start(task) end M.setup = function(cfg) cfg = cfg or {} - globals.setup() + config.setup(cfg) + capabilities.setup() - projects = { Project:new(vim.loop.cwd()) } - set_current_project() + project = Project:new(vim.loop.cwd()) + if not project then + print("cmake-explorer: no CMakeLists.txt file found. Aborting setup") + return + end + project:scan_build_dirs() local cmd = vim.api.nvim_create_user_command - cmd("CMakeConfigure", function(opts) - if #opts.fargs ~= 0 then - M.configure({ build_type = opts.fargs[1] }) - else - M.configure() - end - end, { -- opts - nargs = "*", + cmd("CMakeConfigure", M.configure, { -- opts + nargs = 0, bang = true, - desc = "CMake configure", + desc = "CMake configure with parameters", }) - cmd("CMakeListBuildDirs", M.list_build_dirs, { nargs = 0 }) + cmd( + "CMakeConfigureLast", + M.configure_last, + { nargs = 0, bang = true, desc = "CMake configure last if exists. Otherwise default" } + ) + + cmd( + "CMakeConfigureDir", + M.configure_dir, + { nargs = 0, bang = true, desc = "CMake configure last if exists. Otherwise default" } + ) + + cmd("CMakeListBuilds", M.list_build_dirs, { nargs = 0 }) end return M diff --git a/lua/cmake-explorer/build.lua b/lua/cmake-explorer/build.lua deleted file mode 100644 index ead0646..0000000 --- a/lua/cmake-explorer/build.lua +++ /dev/null @@ -1,107 +0,0 @@ -local config = require("cmake-explorer.config") -local Path = require("plenary.path") -local Scandir = require("plenary.scandir") - --- initial action to create query files (before first build) -local init_query_dir = function(path) - Path:new(path, ".cmake", "api", "v1", "query", "codemodel-v2"):touch({ parents = true }) - Path:new(path, ".cmake", "api", "v1", "query", "cmakeFiles-v1"):touch({ parents = true }) - Path:new(path, ".cmake", "api", "v1", "reply"):mkdir({ parents = true }) -end - -local read_reply_dir = function(path) - local index, cmakefiles, codemodel, targets - local reply_dir = Path:new(path, ".cmake", "api", "v1", "reply") - if not reply_dir:exists() then - return - end - index = Scandir.scan_dir(reply_dir:absolute(), { search_pattern = "index*" }) - if #index == 0 then - return - end - index = vim.json.decode(Path:new(index[1]):read()) - for _, object in ipairs(index.objects) do - if object.kind == "codemodel" then - codemodel = vim.json.decode((reply_dir / object.jsonFile):read()) - for _, target in ipairs(codemodel.configurations[1].targets) do - targets = targets or {} - table.insert(targets, vim.json.decode((reply_dir / target.jsonFile):read())) - end - elseif object.kind == "cmakeFiles" then - cmakefiles = vim.json.decode(Path:new(reply_dir / object.jsonFile):read()) - end - end - return index, cmakefiles, codemodel, targets -end - -local Build = {} - -Build.__index = Build - --- new buildsystem -function Build:new(o) - o = o or {} - - local path - if type(o) == "string" then - path = o - elseif type(o) == "table" and o.path then - path = o.path - else - print("Build.new wrong argument. type(o) = " .. type(o)) - return - end - - local obj = { - path = Path:new(path), - index = nil, - cmakefiles = nil, - codemodel = nil, - targets = nil, - } - - obj.path:mkdir({ parents = true }) - init_query_dir(path) - obj.index, obj.cmakefiles, obj.codemodel, obj.targets = read_reply_dir(path) - - setmetatable(obj, Build) - return obj -end - --- update all internals -function Build:update() - self:set_codemodel() - self:set_cmakefiles() -end - -function Build:build() end - -function Build:generator() - return self.index.cmake.generator.name -end - -function Build:build_type() - return self.codemodel.configurations[1].name -end - -function Build:is_multiconfig() - return self.index.cmake.generator.multiConfig -end - -Build.name = function(opts) - return config.build_dir_template:gsub("{buildType}", opts.build_type or config.build_types[1]) -end - -Build.is_build_dir = function(path) - if not (Path:new(path):is_dir()) then - return - end - if not Path:new(path, ".cmake", "api", "v1", "query", "codemodel-v2"):exists() then - return - elseif not Path:new(path, ".cmake", "api", "v1", "query", "cmakeFiles-v1"):exists() then - return - end - return true -end - -return Build diff --git a/lua/cmake-explorer/build_list.lua b/lua/cmake-explorer/build_list.lua new file mode 100644 index 0000000..7abaddc --- /dev/null +++ b/lua/cmake-explorer/build_list.lua @@ -0,0 +1,57 @@ +local Build = require("cmake-explorer.build") + +local BuildFilter = {} + +BuildFilter.__call = function(self, build) + for k, v in pairs(self) do + if type(k) == "string" then + if v ~= build[k] then + return false + end + end + end + return true +end + +local BuildList = { + __newindex = function(t, k, v) + for _, value in ipairs(t) do + if value == v then + return + end + end + rawset(t, k, v) + end, +} + +function BuildList:new() + local obj = { + current = nil, + } + setmetatable(obj, BuildList) + return obj +end + +function BuildList:insert(o) + local build = Build:new(o) + table.insert(self, build) + self.current = build +end + +function BuildList:filter(pred) + pred = pred or {} + local i, n = 0, #self + if type(pred) == "table" then + setmetatable(pred, BuildFilter) + end + return function() + repeat + i = i + 1 + if pred(self[i]) then + return self[i] + end + until i ~= n + end +end + +return BuildList diff --git a/lua/cmake-explorer/capabilities.lua b/lua/cmake-explorer/capabilities.lua new file mode 100644 index 0000000..f3a966c --- /dev/null +++ b/lua/cmake-explorer/capabilities.lua @@ -0,0 +1,42 @@ +local config = require("cmake-explorer.config") + +local multiconfig_generators = { + "Ninja Multi-Config", + "Xcode", + "Visual Studio 12 2013", + "Visual Studio 14 2015", + "Visual Studio 15 2017", + "Visual Studio 16 2019", + "Visual Studio 17 2022", + -- "Green Hills MULTI" +} + +local Capabilities = { + json = nil, +} + +function Capabilities.generators() + local ret = {} + if not Capabilities then + return ret + end + for k, v in pairs(Capabilities.json.generators) do + table.insert(ret, v.name) + end + return vim.fn.reverse(ret) +end + +function Capabilities.is_multiconfig_generator(generator) + return vim.tbl_contains(multiconfig_generators, generator) +end + +function Capabilities.has_fileapi() + return vim.tbl_get(Capabilities.json, "fileApi") ~= nil +end + +Capabilities.setup = function() + local output = vim.fn.system({ config.cmake_cmd, "-E", "capabilities" }) + Capabilities.json = vim.json.decode(output) +end + +return Capabilities diff --git a/lua/cmake-explorer/config.lua b/lua/cmake-explorer/config.lua index 4e1740d..b44c7cd 100644 --- a/lua/cmake-explorer/config.lua +++ b/lua/cmake-explorer/config.lua @@ -1,8 +1,9 @@ local default_config = { cmake_cmd = "cmake", - build_dir_template = "build-{buildType}", + build_dir_template = { "build", "${buildType}", sep = "-", case = nil }, + build_dir = ".", build_types = { "Debug", "Release" }, - options = { "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON" }, + options = {}, } local M = vim.deepcopy(default_config) diff --git a/lua/cmake-explorer/constants.lua b/lua/cmake-explorer/constants.lua deleted file mode 100644 index 3a6fb1f..0000000 --- a/lua/cmake-explorer/constants.lua +++ /dev/null @@ -1,7 +0,0 @@ -local Enum = require("overseer.enum") - -local M = {} - -M.TAG = Enum.new({ "CONFIGURE", "BUILD", "INSTALL", "TEST", "PACK" }) - -return M diff --git a/lua/cmake-explorer/file_api.lua b/lua/cmake-explorer/file_api.lua new file mode 100644 index 0000000..cd83be4 --- /dev/null +++ b/lua/cmake-explorer/file_api.lua @@ -0,0 +1,93 @@ +local capabilities = require("cmake-explorer.capabilities") +local Path = require("plenary.path") +local Scandir = require("plenary.scandir") +local notif = require("cmake-explorer.notification") + +local query_path_suffix = { ".cmake", "api", "v1", "query", "client-cmake-explorer", "query.json" } +local reply_dir_suffix = { ".cmake", "api", "v1", "reply" } + +local FileApi = {} + +FileApi.__index = FileApi + +function FileApi:new(opts) + if not capabilities.has_fileapi() then + notif.notify("No fileapi files", vim.log.levels.ERROR) + return + end + local path + if type(opts) == "string" then + path = opts + end + local obj = { + path = Path:new(path):absolute(), + index = nil, + cmakefiles = nil, + codemodel = nil, + targets = nil, + } + Path:new(path):mkdir({ parents = true }) + setmetatable(obj, FileApi) + return obj +end + +function FileApi:create() + local query = Path:new(self.path, unpack(query_path_suffix)) + if not query:exists() then + if not query:touch({ parents = true }) then + notif.notify("Cannot create query file", vim.log.levels.ERROR) + return + end + query:write(vim.json.encode(capabilities.json.fileApi), "w") + end + Path:new(self.path, unpack(reply_dir_suffix)):mkdir({ parents = true }) + return true +end + +function FileApi:read_reply() + if not self:reply_exists() then + notif.notify("No reply directory", vim.log.levels.ERROR) + return + end + local reply_dir = Path:new(self.path, unpack(reply_dir_suffix)) + local index = Scandir.scan_dir(tostring(reply_dir), { search_pattern = "index*" }) + if #index == 0 then + notif.notify("No files in reply", vim.log.levels.ERROR) + return + end + self.index = vim.json.decode(Path:new(index[1]):read()) + for _, object in ipairs(self.index.objects) do + if object.kind == "codemodel" then + self.codemodel = vim.json.decode((reply_dir / object.jsonFile):read()) + self.targets = {} + for _, target in ipairs(self.codemodel.configurations[1].targets) do + table.insert(self.targets, vim.json.decode((reply_dir / target.jsonFile):read())) + end + elseif object.kind == "cmakeFiles" then + self.cmakefiles = vim.json.decode(Path:new(reply_dir / object.jsonFile):read()) + end + end + return true +end + +function FileApi:query_exists() + return Path:new(self.path, unpack(query_path_suffix)):exists() +end + +function FileApi:reply_exists() + local reply_dir = Path:new(self.path, unpack(reply_dir_suffix)) + if not reply_dir:exists() then + return + end + return true +end + +function FileApi:exists() + return self:query_exists() and self:reply_exists() +end + +function FileApi.is_fileapi(other) + return getmetatable(other) == FileApi +end + +return FileApi diff --git a/lua/cmake-explorer/globals.lua b/lua/cmake-explorer/globals.lua deleted file mode 100644 index 759bba4..0000000 --- a/lua/cmake-explorer/globals.lua +++ /dev/null @@ -1,29 +0,0 @@ -local config = require("cmake-explorer.config") - -local M = { - capabilities = nil, - generators = {}, -} - -local available_generators = function(capabilities) - local ret = {} - if not capabilities or not capabilities.generators then - return ret - end - for k, v in pairs(capabilities.generators) do - table.insert(ret, v.name) - end - return vim.fn.reverse(ret) -end - -local set_capabilities = function() - local output = vim.fn.system({ config.cmake_cmd, "-E", "capabilities" }) - M.capabilities = vim.json.decode(output) - M.generators = available_generators(M.capabilities) -end - -M.setup = function() - set_capabilities() -end - -return M diff --git a/lua/cmake-explorer/notification.lua b/lua/cmake-explorer/notification.lua new file mode 100644 index 0000000..fa8ff6e --- /dev/null +++ b/lua/cmake-explorer/notification.lua @@ -0,0 +1,13 @@ +local has_notify, notify = pcall(require, "notify") + +local Notification = {} + +function Notification.notify(msg, lvl, opts) + opts = opts or {} + if has_notify then + opts.hide_from_history = true + return notify(msg, lvl, opts) + end +end + +return Notification diff --git a/lua/cmake-explorer/project.lua b/lua/cmake-explorer/project.lua index 967ec1b..bdd54e3 100644 --- a/lua/cmake-explorer/project.lua +++ b/lua/cmake-explorer/project.lua @@ -1,48 +1,10 @@ local config = require("cmake-explorer.config") -local globals = require("cmake-explorer.globals") -local Build = require("cmake-explorer.build") +local capabilities = require("cmake-explorer.capabilities") +local FileApi = require("cmake-explorer.file_api") local Path = require("plenary.path") local Scandir = require("plenary.scandir") local utils = require("cmake-explorer.utils") - -local get_builds_in_dir = function(path) - local ret = {} - -- add to builds directories which accept is_build_dir() - local candidates = Scandir.scan_dir(path, { hidden = false, only_dirs = true, depth = 0, silent = true }) - for _, v in ipairs(candidates) do - if Build.is_build_dir(v) then - local b = Build:new(v) - table.insert(ret, b) - end - end - return ret -end - -local set_current_build = function(builds, filter) - local filter_func - if type(filter) == "number" then - if filter >= 1 and filter <= #builds then - return builds[filter] - else - print("set_current_build. index out of range. set to first") - return builds[1] - end - elseif type(filter) == "string" then - filter_func = function(v) - return v:build_type() == filter - end - elseif type(filter) == "function" then - filter_func = filter - else - return builds[1] - end - for _, v in ipairs(builds) do - if filter_func(v) == true then - return v - end - end - return builds[1] -end +local notif = require("cmake-explorer.notification") local Project = {} @@ -65,69 +27,82 @@ function Project:new(o) end local obj = { - path = Path:new(path), - builds = nil, - current_build = nil, + path = Path:new(path):absolute(), + fileapis = {}, + last_generate = {}, } - obj.builds = get_builds_in_dir(path) - obj.current_build = set_current_build(obj.builds, config.build_types[1]) setmetatable(obj, Project) return obj end --- finds build with passed params, creates new build if not found -function Project:append_build(params) - local build_dir = (self.path / Build.name(params)):absolute() - for _, v in ipairs(self.builds) do - if v.path:absolute() == build_dir then - print("append_build. build found") - return v +function Project:scan_build_dirs() + local candidates = Scandir.scan_dir(self.path, { hidden = false, only_dirs = true, depth = 0, silent = true }) + for _, v in ipairs(candidates) do + local fa = FileApi:new(v) + if fa and fa:exists() and fa:read_reply() then + self.fileapis[v] = fa end end - print("append_build. new build") - table.insert(self.builds, Build:new(build_dir)) - return self.builds[#self.builds] end -function Project:symlink_compile_commands() - local src = (self.current_build.path / "compile_commands.json") +function Project:symlink_compile_commands(path) + local src = Path:new(path, "compile_commands.json") if src:exists() then vim.cmd( 'silent exec "!' - .. config.cmake_cmd - .. " -E create_symlink " - .. src:absolute() - .. " " - .. (self.path / "compile_commands.json"):absolute() - .. '"' + .. config.cmake_cmd + .. " -E create_symlink " + .. src:absolute() + .. " " + .. Path:new(self.path, "compile_commands.json"):absolute() + .. '"' ) end end -function Project:list_build_dirs_names() - local ret = {} - for _, v in ipairs(self.builds) do - table.insert(ret, v.path:absolute()) - end - return ret -end - function Project:configure(params) params = params or {} + local args = utils.generate_args(params, self.path) + local build_dir = utils.build_path(params, self.path) + if not args then + return + end + if not self.fileapis[build_dir] then + local fa = FileApi:new(build_dir) + if not fa then + notif.notify("Cannot fileapi object", vim.log.levels.ERROR) + return + end + if not fa:create() then + return + end + self.fileapis[build_dir] = fa + end - self.current_build = self:append_build(params) - local args = vim.tbl_deep_extend("keep", params.args or {}, config.options or {}) - table.insert(args, "-G" .. (params.generator or globals.generators[1])) - table.insert(args, "-DCMAKE_BUILD_TYPE=" .. (params.build_type or config.build_types[1])) - table.insert(args, "-S" .. self.path:absolute()) - table.insert(args, "-B" .. self.current_build.path:absolute()) return { cmd = config.cmake_cmd, args = args, + cwd = Path:new(self.path):absolute(), after_success = function() - self:symlink_compile_commands() + self.last_generate = build_dir + self.fileapis[build_dir]:read_reply() + self:symlink_compile_commands(build_dir) end, } end +function Project:configure_last() + return self:configure(self.last_generate) +end + +function Project:list_build_dirs() + local ret = {} + for k, _ in pairs(self.fileapis) do + local build = {} + build.path = k + table.insert(ret, build) + end + return ret +end + return Project diff --git a/lua/cmake-explorer/runner.lua b/lua/cmake-explorer/runner.lua index f9c3d84..ca596ac 100644 --- a/lua/cmake-explorer/runner.lua +++ b/lua/cmake-explorer/runner.lua @@ -20,6 +20,11 @@ function M.start(command) on_exit = vim.schedule_wrap(function(_, code, signal) if code == 0 and signal == 0 and command.after_success then command.after_success() + else + vim.notify( + "Code " .. tostring(code) .. ": " .. command.cmd .. " " .. table.concat(command.args, " "), + vim.log.levels.ERROR + ) end end), }) diff --git a/lua/cmake-explorer/utils.lua b/lua/cmake-explorer/utils.lua index 8fff940..e944642 100644 --- a/lua/cmake-explorer/utils.lua +++ b/lua/cmake-explorer/utils.lua @@ -1,25 +1,106 @@ +local config = require("cmake-explorer.config") +local capabilities = require("cmake-explorer.capabilities") +local Path = require("plenary.path") + local utils = { plugin_prefix = "CM", } -function utils.with_prefix(command) - return utils.plugin_prefix .. command +utils.build_dir_name = function(params) + if capabilities.is_multiconfig_generator(params.generator) then + return config.build_dir_template[1] + else + local paths = {} + for k, v in ipairs(config.build_dir_template) do + local path = v:gsub("${buildType}", params.build_type) + if k ~= 1 and config.build_dir_template.case then + if config.build_dir_template.case == "lower" then + path = string.lower(path) + elseif config.build_dir_template.case == "upper" then + path = string.upper(path) + end + end + table.insert(paths, path) + end + return table.concat(paths, config.build_dir_template.sep) + end end -function utils.has_value(tab, val) - for index, value in ipairs(tab) do - if type(val) == "function" then - if val(value) then - return true - end +utils.build_path = function(params, source_dir) + if type(params) == "string" then + return params + end + local build_path = Path:new(config.build_dir) + if build_path:is_absolute() then + return (build_path / utils.build_dir_name(params)):absolute() + else + return Path:new(source_dir, build_path, utils.build_dir_name(params)):absolute() + end +end + +utils.generate_args = function(params, source_dir) + local ret = {} + + if type(params) == "string" then + table.insert(ret, "-B" .. Path:new(params):make_relative(source_dir)) + else + if params.preset then + table.insert(ret, "--preset " .. params.preset) else - if value == val then - return true + if params.generator and vim.tbl_contains(capabilities.generators(), params.generator) then + table.insert(ret, "-G" .. params.generator) + end + + params.build_type = params.build_type or config.build_types[1] + if params.build_type then + table.insert(ret, "-DCMAKE_BUILD_TYPE=" .. params.build_type) + end + + table.insert(ret, "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON") + + if type(params.args) == "table" then + for k, v in pairs(params.args) do + table.insert(ret, "-D" .. k .. "=" .. v) + end + elseif type(params.args) == "string" then + table.insert(ret, params.args) end + table.insert(ret, "-B" .. Path:new(utils.build_path(params, source_dir)):make_relative(source_dir)) + end + end + return ret +end + +utils.is_eq = function(val, cmp, if_eq, if_not_eq) + if val == cmp then + if if_eq then + return if_eq + else + return val + end + else + if if_not_eq then + return if_not_eq + else + return nil end end +end - return false +utils.is_neq = function(val, cmp, if_eq, if_not_eq) + if val ~= cmp then + if if_eq then + return if_eq + else + return val + end + else + if if_not_eq then + return if_not_eq + else + return nil + end + end end return utils -- cgit v1.2.3