aboutsummaryrefslogtreecommitdiff
path: root/lua/cmake
diff options
context:
space:
mode:
Diffstat (limited to 'lua/cmake')
-rw-r--r--lua/cmake/actions.lua111
-rw-r--r--lua/cmake/capabilities.lua63
-rw-r--r--lua/cmake/commands.lua31
-rw-r--r--lua/cmake/config.lua66
-rw-r--r--lua/cmake/fileapi.lua98
-rw-r--r--lua/cmake/init.lua19
-rw-r--r--lua/cmake/lyaml.lua671
-rw-r--r--lua/cmake/project.lua197
-rw-r--r--lua/cmake/telescope/make_entry.lua57
-rw-r--r--lua/cmake/telescope/pickers.lua102
-rw-r--r--lua/cmake/telescope/previewers.lua40
-rw-r--r--lua/cmake/terminal.lua90
-rw-r--r--lua/cmake/utils.lua96
-rw-r--r--lua/cmake/variants.lua157
14 files changed, 1798 insertions, 0 deletions
diff --git a/lua/cmake/actions.lua b/lua/cmake/actions.lua
new file mode 100644
index 0000000..d330a0a
--- /dev/null
+++ b/lua/cmake/actions.lua
@@ -0,0 +1,111 @@
+local pr = require("cmake.project")
+local config = require("cmake.config")
+local t = require("cmake.terminal")
+
+local M = {}
+
+local default_generate_exe_opts = {
+ notify = {
+ ok_message = "CMake build finished",
+ err_message = function(code)
+ return "CMake generate failed with code " .. tostring(code)
+ end,
+ },
+}
+
+local default_build_exe_opts = {
+ notify = {
+ ok_message = "CMake build finished",
+ err_message = function(code)
+ return "CMake build failed with code " .. tostring(code)
+ end,
+ },
+}
+
+M.generate = function(opts)
+ pr.create_fileapi_query({ idx = pr.current_generate_option_idx() }, function()
+ vim.schedule(function()
+ t.cmake_execute(pr.current_generate_option().generate_command, default_generate_exe_opts)
+ end)
+ end)
+end
+
+M.generate_select = function(opts)
+ local items = pr.generate_options(opts)
+ vim.ui.select(items, {
+ prompt = "Select configuration to generate:",
+ format_item = function(item)
+ return table.concat(item.name, config.variants_display.short.sep)
+ end,
+ }, function(choice, idx)
+ if not idx then
+ return
+ end
+ pr.set_current_generate_option(idx)
+ pr.create_fileapi_query({ idx = idx }, function()
+ vim.schedule(function()
+ t.cmake_execute(choice.generate_command, default_generate_exe_opts)
+ end)
+ end)
+ end)
+end
+
+M.build = function(opts)
+ if not pr.current_build_option_idx() then
+ M.build_select(opts)
+ else
+ pr.create_fileapi_query({ idx = pr.current_build_option_idx() }, function()
+ vim.schedule(function()
+ t.cmake_execute(pr.current_build_option().command, default_build_exe_opts)
+ end)
+ end)
+ end
+end
+
+M.build_select = function(opts)
+ local items = pr.current_generate_option(opts).build_options
+ vim.ui.select(items, {
+ prompt = "Select build option to generate:",
+ format_item = function(item)
+ return table.concat(item.name, config.variants_display.short.sep)
+ end,
+ }, function(choice, idx)
+ if not idx then
+ return
+ end
+ pr.set_current_build_option(idx)
+ pr.create_fileapi_query({ idx = idx }, function()
+ vim.schedule(function()
+ t.cmake_execute(choice.command, default_build_exe_opts)
+ end)
+ end)
+ end)
+end
+
+M.run_tagret = function(opts)
+ return
+end
+
+M.run_tagret_select = function(opts)
+ opts = opts or {}
+ opts.type = "EXECUTABLE"
+ local items = pr.current_targets(opts)
+ vim.ui.select(items, {
+ prompt = "Select tagret to run:",
+ format_item = function(item)
+ return item.name
+ end,
+ }, function(choice, idx)
+ if not idx then
+ return
+ end
+ pr.set_current_executable_target(idx)
+ local command = {
+ cwd = pr.current_directory(),
+ cmd = choice.path,
+ }
+ t.target_execute(command)
+ end)
+end
+
+return M
diff --git a/lua/cmake/capabilities.lua b/lua/cmake/capabilities.lua
new file mode 100644
index 0000000..6a70be2
--- /dev/null
+++ b/lua/cmake/capabilities.lua
@@ -0,0 +1,63 @@
+local config = require("cmake.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)
+ -- if generator is nil, assume is is not multiconifg
+ if not generator then
+ return
+ end
+ return vim.tbl_contains(multiconfig_generators, generator)
+end
+
+function Capabilities.has_fileapi()
+ return vim.tbl_get(Capabilities.json, "fileApi") ~= nil
+end
+
+-- TODO: make this async
+Capabilities.setup = function(callback)
+ local lines = {}
+ vim.fn.jobstart({ config.cmake.cmake_path, "-E", "capabilities" }, {
+ on_stdout = function(_, data)
+ if data then
+ vim.list_extend(lines, data)
+ end
+ end,
+ on_exit = function(_, code, _)
+ if code == 0 then
+ Capabilities.json = vim.json.decode(table.concat(lines, ""))
+ if type(callback) == "function" then
+ callback()
+ end
+ else
+ vim.notify("error " .. tostring(code) .. ". 'cmake -E capabilities'", vim.log.levels.ERROR)
+ end
+ end,
+ })
+end
+
+return Capabilities
diff --git a/lua/cmake/commands.lua b/lua/cmake/commands.lua
new file mode 100644
index 0000000..78e5bb8
--- /dev/null
+++ b/lua/cmake/commands.lua
@@ -0,0 +1,31 @@
+local M = {}
+
+local cmd = vim.api.nvim_create_user_command
+
+M.register_commands = function()
+ cmd("CMakeGenerate", function()
+ require("cmake.actions").generate()
+ end, { desc = "Generate with last configuration" })
+
+ cmd("CMakeGenerateSelect", function()
+ require("cmake.actions").generate_select()
+ end, { desc = "Select configuration and generate" })
+
+ cmd("CMakeBuild", function()
+ require("cmake.actions").build()
+ end, { desc = "Build with last build option" })
+
+ cmd("CMakeBuildSelect", function()
+ require("cmake.actions").build_select()
+ end, { desc = "Select build option and build" })
+
+ cmd("CMakeRun", function()
+ require("cmake.actions").run_tagret()
+ end, { desc = "Select build option and build" })
+
+ cmd("CMakeRunSelect", function()
+ require("cmake.actions").run_tagret_select()
+ end, { desc = "Select build option and build" })
+end
+
+return M
diff --git a/lua/cmake/config.lua b/lua/cmake/config.lua
new file mode 100644
index 0000000..9a0e5ba
--- /dev/null
+++ b/lua/cmake/config.lua
@@ -0,0 +1,66 @@
+local default_config = {
+ cmake = {
+ cmake_path = "cmake",
+ environment = {},
+ configure_environment = {},
+ build_directory = "${workspaceFolder}/build-${buildType}",
+ build_environment = {},
+ build_args = {},
+ build_tool_args = {},
+ generator = nil,
+ variants = {
+ {
+ default = "debug",
+ description = "Build type",
+ choices = {
+ debug = { short = "Debug", long = "Long debug", buildType = "Debug" },
+ release = { short = "Release", long = "Long release", buildType = "Release" },
+ },
+ },
+ {
+ default = "static",
+ choices = {
+ static = { short = "Static", long = "Long static", linkage = "static" },
+ shared = { short = "Shared", long = "Long shared", linkage = "shared" },
+ },
+ },
+ },
+ parallel_jobs = 0,
+ save_before_build = true,
+ source_directory = "${workspaceFolder}",
+ },
+ terminal = {
+ direction = "horizontal",
+ display_name = "CMake",
+ close_on_exit = "success",
+ hidden = false,
+ clear_env = false,
+ focus = false,
+ },
+ runner_terminal = {
+ direction = "horizontal",
+ close_on_exit = false,
+ hidden = false,
+ clear_env = false,
+ focus = true,
+ },
+ notification = {
+ after = "success",
+ },
+ variants_display = {
+ short = { sep = " × " },
+ long = { sep = " ❄ " },
+ },
+}
+
+local M = vim.deepcopy(default_config)
+
+M.setup = function(opts)
+ local newconf = vim.tbl_deep_extend("force", default_config, opts or {})
+
+ for k, v in pairs(newconf) do
+ M[k] = v
+ end
+end
+
+return M
diff --git a/lua/cmake/fileapi.lua b/lua/cmake/fileapi.lua
new file mode 100644
index 0000000..2f61677
--- /dev/null
+++ b/lua/cmake/fileapi.lua
@@ -0,0 +1,98 @@
+local capabilities = require("cmake.capabilities")
+local Path = require("plenary.path")
+local scan = require("plenary.scandir")
+local utils = require("cmake.utils")
+local uv = vim.loop
+
+local query_path_suffix = { ".cmake", "api", "v1", "query", "client-cmake", "query.json" }
+local reply_dir_suffix = { ".cmake", "api", "v1", "reply" }
+
+local FileApi = {}
+
+function FileApi.create(path, callback)
+ local query = Path:new(path, unpack(query_path_suffix)):normalize()
+ utils.file_exists(query, function(exists)
+ if not exists then
+ if capabilities.json.fileApi then
+ vim.schedule(function()
+ --TODO: change to async
+ vim.fn.mkdir(Path:new(vim.fs.dirname(query)):absolute(), "p")
+ utils.write_file(query, vim.json.encode(capabilities.json.fileApi), callback)
+ end)
+ else
+ vim.notify("Bad fileApi ", vim.log.levels.ERROR)
+ end
+ else
+ callback()
+ end
+ end)
+end
+
+function FileApi.read_reply(path, callback)
+ local reply_dir = Path:new(path, unpack(reply_dir_suffix)):absolute()
+ utils.file_exists(reply_dir, function(exists)
+ if not exists then
+ return
+ end
+ local ret = { targets = {} }
+ scan.scan_dir_async(reply_dir, {
+ search_pattern = "index*",
+ on_exit = function(results)
+ if #results == 0 then
+ return
+ end
+ utils.read_file(results[1], function(index_data)
+ local index = vim.json.decode(index_data)
+ for _, object in ipairs(index.objects) do
+ if object.kind == "codemodel" then
+ utils.read_file(Path:new(reply_dir, object.jsonFile):absolute(), function(codemodel_data)
+ local codemodel = vim.json.decode(codemodel_data)
+ for _, target in ipairs(codemodel.configurations[1].targets) do
+ utils.read_file(
+ Path:new(reply_dir, target.jsonFile):absolute(),
+ function(target_data)
+ local target_json = vim.json.decode(target_data)
+ local _target = {
+ id = target_json.id,
+ name = target_json.name,
+ type = target_json.type,
+ }
+ if target_json.artifacts then
+ --NOTE: add_library(<name> OBJECT ...) could contain more than ohe object in artifacts
+ -- so maybe in future it will be useful to handle not only first one. Current behaviour
+ -- aims to get path for only EXECUTABLE targets
+ _target.path = target_json.artifacts[1].path
+ end
+ callback(_target)
+ end
+ )
+ end
+ end)
+ end
+ end
+ end)
+ end,
+ })
+ return ret
+ end)
+end
+
+function FileApi.query_exists(path, callback)
+ utils.file_exists(Path:new(path, unpack(query_path_suffix)):normalize(), function(query_exists)
+ callback(query_exists)
+ end)
+end
+
+function FileApi.exists(path, callback)
+ FileApi.query_exists(path, function(query_exists)
+ if not query_exists then
+ callback(false)
+ else
+ utils.file_exists(Path:new(path, unpack(reply_dir_suffix)):normalize(), function(reply_exists)
+ callback(reply_exists)
+ end)
+ end
+ end)
+end
+
+return FileApi
diff --git a/lua/cmake/init.lua b/lua/cmake/init.lua
new file mode 100644
index 0000000..d53e074
--- /dev/null
+++ b/lua/cmake/init.lua
@@ -0,0 +1,19 @@
+local config = require("cmake.config")
+local commands = require("cmake.commands")
+
+local M = {}
+
+function M.setup(opts)
+ opts = opts or {}
+ config.setup(opts)
+ if vim.fn.executable(config.cmake.cmake_path) then
+ commands.register_commands()
+ require("cmake.capabilities").setup(function()
+ require("cmake.project").setup(opts)
+ end)
+ else
+ vim.notify("CMake: " .. config.cmake.cmake_path .. " is not executable", vim.log.levels.WARN)
+ end
+end
+
+return M
diff --git a/lua/cmake/lyaml.lua b/lua/cmake/lyaml.lua
new file mode 100644
index 0000000..9555a75
--- /dev/null
+++ b/lua/cmake/lyaml.lua
@@ -0,0 +1,671 @@
+--[[
+(The MIT License)
+
+Copyright (c) 2017 Dominic Letz dominicletz@exosite.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the 'Software'), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+--]]
+
+local table_print_value
+table_print_value = function(value, indent, done)
+ indent = indent or 0
+ done = done or {}
+ if type(value) == "table" and not done[value] then
+ done[value] = true
+
+ local list = {}
+ for key in pairs(value) do
+ list[#list + 1] = key
+ end
+ table.sort(list, function(a, b)
+ return tostring(a) < tostring(b)
+ end)
+ local last = list[#list]
+
+ local rep = "{\n"
+ local comma
+ for _, key in ipairs(list) do
+ if key == last then
+ comma = ""
+ else
+ comma = ","
+ end
+ local keyRep
+ if type(key) == "number" then
+ keyRep = key
+ else
+ keyRep = string.format("%q", tostring(key))
+ end
+ rep = rep
+ .. string.format(
+ "%s[%s] = %s%s\n",
+ string.rep(" ", indent + 2),
+ keyRep,
+ table_print_value(value[key], indent + 2, done),
+ comma
+ )
+ end
+
+ rep = rep .. string.rep(" ", indent) -- indent it
+ rep = rep .. "}"
+
+ done[value] = false
+ return rep
+ elseif type(value) == "string" then
+ return string.format("%q", value)
+ else
+ return tostring(value)
+ end
+end
+
+local table_print = function(tt)
+ print("return " .. table_print_value(tt))
+end
+
+local table_clone = function(t)
+ local clone = {}
+ for k, v in pairs(t) do
+ clone[k] = v
+ end
+ return clone
+end
+
+local string_trim = function(s, what)
+ what = what or " "
+ return s:gsub("^[" .. what .. "]*(.-)[" .. what .. "]*$", "%1")
+end
+
+local push = function(stack, item)
+ stack[#stack + 1] = item
+end
+
+local pop = function(stack)
+ local item = stack[#stack]
+ stack[#stack] = nil
+ return item
+end
+
+local context = function(str)
+ if type(str) ~= "string" then
+ return ""
+ end
+
+ str = str:sub(0, 25):gsub("\n", "\\n"):gsub('"', '\\"')
+ return ', near "' .. str .. '"'
+end
+
+local Parser = {}
+function Parser.new(self, tokens)
+ self.tokens = tokens
+ self.parse_stack = {}
+ self.refs = {}
+ self.current = 0
+ return self
+end
+
+local exports = { version = "1.2" }
+
+local word = function(w)
+ return "^(" .. w .. ")([%s$%c])"
+end
+
+local tokens = {
+ { "comment", "^#[^\n]*" },
+ { "indent", "^\n( *)" },
+ { "space", "^ +" },
+ {
+ "true",
+ word("enabled"),
+ const = true,
+ value = true,
+ },
+ {
+ "true",
+ word("true"),
+ const = true,
+ value = true,
+ },
+ {
+ "true",
+ word("yes"),
+ const = true,
+ value = true,
+ },
+ {
+ "true",
+ word("on"),
+ const = true,
+ value = true,
+ },
+ {
+ "false",
+ word("disabled"),
+ const = true,
+ value = false,
+ },
+ {
+ "false",
+ word("false"),
+ const = true,
+ value = false,
+ },
+ {
+ "false",
+ word("no"),
+ const = true,
+ value = false,
+ },
+ {
+ "false",
+ word("off"),
+ const = true,
+ value = false,
+ },
+ {
+ "null",
+ word("null"),
+ const = true,
+ value = nil,
+ },
+ {
+ "null",
+ word("Null"),
+ const = true,
+ value = nil,
+ },
+ {
+ "null",
+ word("NULL"),
+ const = true,
+ value = nil,
+ },
+ {
+ "null",
+ word("~"),
+ const = true,
+ value = nil,
+ },
+ { "id", '^"([^"]-)" *(:[%s%c])' },
+ { "id", "^'([^']-)' *(:[%s%c])" },
+ { "string", '^"([^"]-)"', force_text = true },
+ { "string", "^'([^']-)'", force_text = true },
+ { "timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?):(%d%d)" },
+ { "timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?)" },
+ { "timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)" },
+ { "timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d)" },
+ { "timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?)" },
+ { "timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)" },
+ { "doc", "^%-%-%-[^%c]*" },
+ { ",", "^," },
+ { "string", "^%b{} *[^,%c]+", noinline = true },
+ { "{", "^{" },
+ { "}", "^}" },
+ { "string", "^%b[] *[^,%c]+", noinline = true },
+ { "[", "^%[" },
+ { "]", "^%]" },
+ { "-", "^%-", noinline = true },
+ { ":", "^:" },
+ { "pipe", "^(|)(%d*[+%-]?)", sep = "\n" },
+ { "pipe", "^(>)(%d*[+%-]?)", sep = " " },
+ { "id", "^([%w][%w %-_]*)(:[%s%c])" },
+ { "string", "^[^%c]+", noinline = true },
+ { "string", "^[^,%]}%c ]+" },
+}
+exports.tokenize = function(str)
+ local token
+ local row = 0
+ local ignore
+ local indents = 0
+ local lastIndents
+ local stack = {}
+ local indentAmount = 0
+ local inline = false
+ str = str:gsub("\r\n", "\010")
+
+ while #str > 0 do
+ for i in ipairs(tokens) do
+ local captures = {}
+ if not inline or tokens[i].noinline == nil then
+ captures = { str:match(tokens[i][2]) }
+ end
+
+ if #captures > 0 then
+ captures.input = str:sub(0, 25)
+ token = table_clone(tokens[i])
+ token[2] = captures
+ local str2 = str:gsub(tokens[i][2], "", 1)
+ token.raw = str:sub(1, #str - #str2)
+ str = str2
+
+ if token[1] == "{" or token[1] == "[" then
+ inline = true
+ elseif token.const then
+ -- Since word pattern contains last char we're re-adding it
+ str = token[2][2] .. str
+ token.raw = token.raw:sub(1, #token.raw - #token[2][2])
+ elseif token[1] == "id" then
+ -- Since id pattern contains last semi-colon we're re-adding it
+ str = token[2][2] .. str
+ token.raw = token.raw:sub(1, #token.raw - #token[2][2])
+ -- Trim
+ token[2][1] = string_trim(token[2][1])
+ elseif token[1] == "string" then
+ -- Finding numbers
+ local snip = token[2][1]
+ if not token.force_text then
+ if snip:match("^(-?%d+%.%d+)$") or snip:match("^(-?%d+)$") then
+ token[1] = "number"
+ end
+ end
+ elseif token[1] == "comment" then
+ ignore = true
+ elseif token[1] == "indent" then
+ row = row + 1
+ inline = false
+ lastIndents = indents
+ if indentAmount == 0 then
+ indentAmount = #token[2][1]
+ end
+
+ if indentAmount ~= 0 then
+ indents = (#token[2][1] / indentAmount)
+ else
+ indents = 0
+ end
+
+ if indents == lastIndents then
+ ignore = true
+ elseif indents > lastIndents + 2 then
+ error(
+ "SyntaxError: invalid indentation, got "
+ .. tostring(indents)
+ .. " instead of "
+ .. tostring(lastIndents)
+ .. context(token[2].input)
+ )
+ elseif indents > lastIndents + 1 then
+ push(stack, token)
+ elseif indents < lastIndents then
+ local input = token[2].input
+ token = { "dedent", { "", input = "" } }
+ token.input = input
+ while lastIndents > indents + 1 do
+ lastIndents = lastIndents - 1
+ push(stack, token)
+ end
+ end
+ end -- if token[1] == XXX
+ token.row = row
+ break
+ end -- if #captures > 0
+ end
+
+ if not ignore then
+ if token then
+ push(stack, token)
+ token = nil
+ else
+ error("SyntaxError " .. context(str))
+ end
+ end
+
+ ignore = false
+ end
+
+ return stack
+end
+
+Parser.peek = function(self, offset)
+ offset = offset or 1
+ return self.tokens[offset + self.current]
+end
+
+Parser.advance = function(self)
+ self.current = self.current + 1
+ return self.tokens[self.current]
+end
+
+Parser.advanceValue = function(self)
+ return self:advance()[2][1]
+end
+
+Parser.accept = function(self, type)
+ if self:peekType(type) then
+ return self:advance()
+ end
+end
+
+Parser.expect = function(self, type, msg)
+ return self:accept(type) or error(msg .. context(self:peek()[1].input))
+end
+
+Parser.expectDedent = function(self, msg)
+ return self:accept("dedent") or (self:peek() == nil) or error(msg .. context(self:peek()[2].input))
+end
+
+Parser.peekType = function(self, val, offset)
+ return self:peek(offset) and self:peek(offset)[1] == val
+end
+
+Parser.ignore = function(self, items)
+ local advanced
+ repeat
+ advanced = false
+ for _, v in pairs(items) do
+ if self:peekType(v) then
+ self:advance()
+ advanced = true
+ end
+ end
+ until advanced == false
+end
+
+Parser.ignoreSpace = function(self)
+ self:ignore({ "space" })
+end
+
+Parser.ignoreWhitespace = function(self)
+ self:ignore({ "space", "indent", "dedent" })
+end
+
+Parser.parse = function(self)
+ local ref = nil
+ if self:peekType("string") and not self:peek().force_text then
+ local char = self:peek()[2][1]:sub(1, 1)
+ if char == "&" then
+ ref = self:peek()[2][1]:sub(2)
+ self:advanceValue()
+ self:ignoreSpace()
+ elseif char == "*" then
+ ref = self:peek()[2][1]:sub(2)
+ return self.refs[ref]
+ end
+ end
+
+ local result
+ local c = {
+ indent = self:accept("indent") and 1 or 0,
+ token = self:peek(),
+ }
+ push(self.parse_stack, c)
+
+ if c.token[1] == "doc" then
+ result = self:parseDoc()
+ elseif c.token[1] == "-" then
+ result = self:parseList()
+ elseif c.token[1] == "{" then
+ result = self:parseInlineHash()
+ elseif c.token[1] == "[" then
+ result = self:parseInlineList()
+ elseif c.token[1] == "id" then
+ result = self:parseHash()
+ elseif c.token[1] == "string" then
+ result = self:parseString("\n")
+ elseif c.token[1] == "timestamp" then
+ result = self:parseTimestamp()
+ elseif c.token[1] == "number" then
+ result = tonumber(self:advanceValue())
+ elseif c.token[1] == "pipe" then
+ result = self:parsePipe()
+ elseif c.token.const == true then
+ self:advanceValue()
+ result = c.token.value
+ else
+ error("ParseError: unexpected token '" .. c.token[1] .. "'" .. context(c.token.input))
+ end
+
+ pop(self.parse_stack)
+ while c.indent > 0 do
+ c.indent = c.indent - 1
+ local term = "term " .. c.token[1] .. ": '" .. c.token[2][1] .. "'"
+ self:expectDedent("last " .. term .. " is not properly dedented")
+ end
+
+ if ref then
+ self.refs[ref] = result
+ end
+ return result
+end
+
+Parser.parseDoc = function(self)
+ self:accept("doc")
+ return self:parse()
+end
+
+Parser.inline = function(self)
+ local current = self:peek(0)
+ if not current then
+ return {}, 0
+ end
+
+ local inline = {}
+ local i = 0
+
+ while self:peek(i) and not self:peekType("indent", i) and current.row == self:peek(i).row do
+ inline[self:peek(i)[1]] = true
+ i = i - 1
+ end
+ return inline, -i
+end
+
+Parser.isInline = function(self)
+ local _, i = self:inline()
+ return i > 0
+end
+
+Parser.parent = function(self, level)
+ level = level or 1
+ return self.parse_stack[#self.parse_stack - level]
+end
+
+Parser.parentType = function(self, type, level)
+ return self:parent(level) and self:parent(level).token[1] == type
+end
+
+Parser.parseString = function(self)
+ if self:isInline() then
+ local result = self:advanceValue()
+
+ --[[
+ - a: this looks
+ flowing: but is
+ no: string
+ --]]
+ local types = self:inline()
+ if types["id"] and types["-"] then
+ if not self:peekType("indent") or not self:peekType("indent", 2) then
+ return result
+ end
+ end
+
+ --[[
+ a: 1
+ b: this is
+ a flowing string
+ example
+ c: 3
+ --]]
+ if self:peekType("indent") then
+ self:expect("indent", "text block needs to start with indent")
+ local addtl = self:accept("indent")
+
+ result = result .. "\n" .. self:parseTextBlock("\n")
+
+ self:expectDedent("text block ending dedent missing")
+ if addtl then
+ self:expectDedent("text block ending dedent missing")
+ end
+ end
+ return result
+ else
+ --[[
+ a: 1
+ b:
+ this is also
+ a flowing string
+ example
+ c: 3
+ --]]
+ return self:parseTextBlock("\n")
+ end
+end
+
+Parser.parsePipe = function(self)
+ local pipe = self:expect("pipe")
+ self:expect("indent", "text block needs to start with indent")
+ local result = self:parseTextBlock(pipe.sep)
+ self:expectDedent("text block ending dedent missing")
+ return result
+end
+
+Parser.parseTextBlock = function(self, sep)
+ local token = self:advance()
+ local result = string_trim(token.raw, "\n")
+ local indents = 0
+ while self:peek() ~= nil and (indents > 0 or not self:peekType("dedent")) do
+ local newtoken = self:advance()
+ while token.row < newtoken.row do
+ result = result .. sep
+ token.row = token.row + 1
+ end
+ if newtoken[1] == "indent" then
+ indents = indents + 1
+ elseif newtoken[1] == "dedent" then
+ indents = indents - 1
+ else
+ result = result .. string_trim(newtoken.raw, "\n")
+ end
+ end
+ return result
+end
+
+Parser.parseHash = function(self, hash)
+ hash = hash or {}
+ local indents = 0
+
+ if self:isInline() then
+ local id = self:advanceValue()
+ self:expect(":", "expected semi-colon after id")
+ self:ignoreSpace()
+ if self:accept("indent") then
+ indents = indents + 1
+ hash[id] = self:parse()
+ else
+ hash[id] = self:parse()
+ if self:accept("indent") then
+ indents = indents + 1
+ end
+ end
+ self:ignoreSpace()
+ end
+
+ while self:peekType("id") do
+ local id = self:advanceValue()
+ self:expect(":", "expected semi-colon after id")
+ self:ignoreSpace()
+ hash[id] = self:parse()
+ self:ignoreSpace()
+ end
+
+ while indents > 0 do
+ self:expectDedent("expected dedent")
+ indents = indents - 1
+ end
+
+ return hash
+end
+
+Parser.parseInlineHash = function(self)
+ local id
+ local hash = {}
+ local i = 0
+
+ self:accept("{")
+ while not self:accept("}") do
+ self:ignoreSpace()
+ if i > 0 then
+ self:expect(",", "expected comma")
+ end
+
+ self:ignoreWhitespace()
+ if self:peekType("id") then
+ id = self:advanceValue()
+ if id then
+ self:expect(":", "expected semi-colon after id")
+ self:ignoreSpace()
+ hash[id] = self:parse()
+ self:ignoreWhitespace()
+ end
+ end
+
+ i = i + 1
+ end
+ return hash
+end
+
+Parser.parseList = function(self)
+ local list = {}
+ while self:accept("-") do
+ self:ignoreSpace()
+ list[#list + 1] = self:parse()
+
+ self:ignoreSpace()
+ end
+ return list
+end
+
+Parser.parseInlineList = function(self)
+ local list = {}
+ local i = 0
+ self:accept("[")
+ while not self:accept("]") do
+ self:ignoreSpace()
+ if i > 0 then
+ self:expect(",", "expected comma")
+ end
+
+ self:ignoreSpace()
+ list[#list + 1] = self:parse()
+ self:ignoreSpace()
+ i = i + 1
+ end
+
+ return list
+end
+
+Parser.parseTimestamp = function(self)
+ local capture = self:advance()[2]
+
+ return os.time({
+ year = capture[1],
+ month = capture[2],
+ day = capture[3],
+ hour = capture[4] or 0,
+ min = capture[5] or 0,
+ sec = capture[6] or 0,
+ isdst = false,
+ }) - os.time({ year = 1970, month = 1, day = 1, hour = 8 })
+end
+
+exports.eval = function(str)
+ return Parser:new(exports.tokenize(str)):parse()
+end
+
+exports.dump = table_print
+
+return exports
diff --git a/lua/cmake/project.lua b/lua/cmake/project.lua
new file mode 100644
index 0000000..f98b82a
--- /dev/null
+++ b/lua/cmake/project.lua
@@ -0,0 +1,197 @@
+local config = require("cmake.config")
+local VariantConfig = require("cmake.variants")
+local FileApi = require("cmake.fileapi")
+local lyaml = require("cmake.lyaml")
+local utils = require("cmake.utils")
+local scan = require("plenary.scandir")
+
+local Project = {}
+
+local configs = {}
+local current_config = nil
+local fileapis = {}
+
+local append_after_success_actions = function()
+ local read_reply = function(v, not_presented)
+ if (not_presented and not fileapis[v.directory]) or not not_presented then
+ --TODO: replace to vim.fs.joinpath after nvim 0.10 release
+ utils.symlink(v.directory .. "/compile_commands.json", vim.loop.cwd())
+ fileapis[v.directory] = { targets = {} }
+ FileApi.read_reply(v.directory, function(target)
+ table.insert(fileapis[v.directory].targets, target)
+ end)
+ end
+ end
+ for _, v in ipairs(configs) do
+ v.generate_command.after_success = function()
+ read_reply(v, false)
+ end
+ for _, bv in ipairs(v.build_options) do
+ bv.command.after_success = function()
+ read_reply(v, true)
+ end
+ end
+ end
+end
+
+local init_fileapis = function()
+ fileapis = {}
+ for _, v in ipairs(configs) do
+ if not fileapis[v.directory] then
+ fileapis[v.directory] = { targets = {} }
+ FileApi.exists(v.directory, function(fileapi_exists)
+ if fileapi_exists then
+ FileApi.read_reply(v.directory, function(target)
+ table.insert(fileapis[v.directory].targets, target)
+ end)
+ end
+ end)
+ end
+ end
+end
+
+-- TODO: validate yaml and fallback to config's variants if not valid
+function Project.from_variants(variants)
+ for var, is_default in VariantConfig.cartesian_product(variants) do
+ var.current_build = 1
+ table.insert(configs, var)
+ current_config = not current_config and is_default and #configs or current_config
+ end
+ if not current_config and #configs ~= 0 then
+ current_config = 1
+ end
+ append_after_success_actions()
+ init_fileapis()
+end
+
+function Project.generate_options(opts)
+ opts = opts or {}
+ return configs
+end
+
+--TODO: remove opts where it is useless
+function Project.current_generate_option(opts)
+ opts = opts or {}
+ assert(current_config, "No current project config")
+ return configs[current_config]
+end
+
+function Project.current_generate_option_idx()
+ return current_config
+end
+
+function Project.set_current_generate_option(idx)
+ current_config = idx
+end
+
+--TODO: check on out of range
+function Project.current_build_option_idx()
+ return configs[current_config].current_build
+end
+
+function Project.current_build_option()
+ if not Project.current_build_option_idx() then
+ return nil
+ end
+ return configs[current_config].build_options[Project.current_build_option_idx()]
+end
+
+function Project.set_current_build_option(idx)
+ configs[current_config].current_build = idx
+end
+
+function Project.current_directory()
+ return current_config and configs[current_config].directory or nil
+end
+
+local current_fileapi = function()
+ if not Project.current_directory() or not fileapis[Project.current_directory()] then
+ return nil
+ end
+ return fileapis[Project.current_directory()]
+end
+
+function Project.set_current_executable_target(idx)
+ current_fileapi().current_executable_target = idx
+end
+
+function Project.current_executable_target_idx()
+ local _curr_fileapi = current_fileapi()
+ if not _curr_fileapi then
+ return nil
+ end
+ return _curr_fileapi.current_executable_target
+end
+
+function Project.current_executable_target()
+ local _curr_fileapi = current_fileapi()
+ if not _curr_fileapi then
+ return nil
+ end
+ local _curr_exe_target_idx = Project.current_executable_target_idx()
+ if not _curr_exe_target_idx then
+ return nil
+ end
+ return _curr_fileapi.targets[_curr_exe_target_idx]
+end
+
+function Project.current_targets(opts)
+ opts = opts or {}
+ local _curr_fileapi = current_fileapi()
+ if not _curr_fileapi then
+ return nil
+ end
+ if opts.type then
+ return vim.tbl_filter(function(t)
+ return t.type == opts.type
+ end, _curr_fileapi.targets)
+ end
+ return _curr_fileapi.targets
+end
+
+function Project.create_fileapi_query(opts, callback)
+ opts = opts or {}
+ local path
+
+ if type(opts.idx) == "number" then
+ path = configs[opts.idx].directory
+ elseif type(opts.path) == "string" then
+ path = opts.path
+ --TODO: compare getmetatable(opts.config) with VariantConfig (and PresetsConfig in future)
+ elseif type(opts.config) == "table" then
+ path = opts.config.directory
+ else
+ path = configs[current_config].directory
+ end
+ FileApi.query_exists(path, function(query_exists)
+ if not query_exists then
+ FileApi.create(path, function()
+ callback()
+ end)
+ else
+ callback()
+ end
+ end)
+end
+
+function Project.setup(opts)
+ opts = opts or {}
+ scan.scan_dir_async(".", {
+ depth = 0,
+ hidden = true,
+ silent = true,
+ search_pattern = ".variants.yaml",
+ on_exit = function(variants_results)
+ if #variants_results ~= 0 then
+ utils.read_file(variants_results[1], function(variants_data)
+ local yaml = lyaml.eval(variants_data)
+ Project.from_variants(yaml)
+ end)
+ else
+ Project.from_variants(config.cmake.variants)
+ end
+ end,
+ })
+end
+
+return Project
diff --git a/lua/cmake/telescope/make_entry.lua b/lua/cmake/telescope/make_entry.lua
new file mode 100644
index 0000000..d0b04bf
--- /dev/null
+++ b/lua/cmake/telescope/make_entry.lua
@@ -0,0 +1,57 @@
+local make_entry = require("telescope.make_entry")
+local entry_display = require("telescope.pickers.entry_display")
+local config = require("cmake.config")
+
+local M = {}
+
+M.gen_from_configure = function(opts)
+ local project = require("cmake").project
+ local displayer = entry_display.create({
+ separator = " ",
+ items = {
+ { width = project.display.short_len + 5 },
+ { remaining = true },
+ },
+ })
+ local make_display = function(entry)
+ vim.print(entry)
+ return displayer({
+ { entry.value.display.short, "TelescopeResultsIdentifier" },
+ { entry.value.display.long, "TelescopeResultsComment" },
+ })
+ end
+ return function(entry)
+ return make_entry.set_default_entry_mt({
+ value = entry,
+ ordinal = table.concat(entry.short, config.variants_display.short_sep),
+ display = make_display,
+ }, opts)
+ end
+end
+
+M.gen_from_build = function(opts)
+ local project = require("cmake").project
+ local displayer = entry_display.create({
+ separator = " ",
+ items = {
+ { width = project.display.short_len + 5 },
+ { remaining = true },
+ },
+ })
+ local make_display = function(entry)
+ vim.print(entry)
+ return displayer({
+ { entry.value.display.short, "TelescopeResultsIdentifier" },
+ { entry.value.display.long, "TelescopeResultsComment" },
+ })
+ end
+ return function(entry)
+ return make_entry.set_default_entry_mt({
+ value = entry,
+ ordinal = table.concat(entry.short, config.variants_display.short_sep),
+ display = make_display,
+ }, opts)
+ end
+end
+
+return M
diff --git a/lua/cmake/telescope/pickers.lua b/lua/cmake/telescope/pickers.lua
new file mode 100644
index 0000000..c543df8
--- /dev/null
+++ b/lua/cmake/telescope/pickers.lua
@@ -0,0 +1,102 @@
+local pickers = require("telescope.pickers")
+local finders = require("telescope.finders")
+local conf = require("telescope.config").values
+local actions = require("telescope.actions")
+local action_state = require("telescope.actions.state")
+local cmake_make_entry = require("cmake.telescope.make_entry")
+local previewers = require("cmake.telescope.previewers")
+
+local M = {}
+
+M.build_dirs = function(opts)
+ local cmake = require("cmake")
+ pickers
+ .new(opts, {
+ prompt_title = "CMake Builds",
+ finder = finders.new_table({
+ results = cmake.project.fileapis,
+ -- entry_maker = cmake_make_entry.gen_from_fileapi(opts),
+ entry_maker = function(entry)
+ return {
+ value = entry,
+ display = entry.path,
+ ordinal = entry.path,
+ }
+ end,
+ sorter = conf.generic_sorter(opts),
+ -- attach_mappings = function(prompt_bufnr, map)
+ -- actions.select_default:replace(function() end)
+ -- return true
+ -- end,
+ }),
+ })
+ :find()
+end
+
+M.configure = function(opts)
+ local cmake = require("cmake")
+ local runner = require("cmake.runner")
+ opts.layout_strategy = "vertical"
+ opts.layout_config = {
+ prompt_position = "top",
+ preview_cutoff = 0,
+ preview_height = 5,
+ mirror = true,
+ }
+ pickers
+ .new(opts, {
+ default_selection_index = cmake.project:current_configure_index(),
+ prompt_title = "CMake Configure Options",
+ finder = finders.new_table({
+ results = cmake.project:list_configs(),
+ entry_maker = cmake_make_entry.gen_from_configure(opts),
+ }),
+ sorter = conf.generic_sorter(opts),
+ previewer = previewers.configure_previewer(),
+ attach_mappings = function(prompt_bufnr, map)
+ actions.select_default:replace(function()
+ actions.close(prompt_bufnr)
+ local selection = action_state.get_selected_entry()
+ cmake.project.current_config = selection.value
+ runner.start(selection.value.generate_command)
+ end)
+ return true
+ end,
+ })
+ :find()
+end
+
+M.build = function(opts)
+ local cmake = require("cmake")
+ local runner = require("cmake.runner")
+ opts.layout_strategy = "vertical"
+ opts.layout_config = {
+ prompt_position = "top",
+ preview_cutoff = 0,
+ preview_height = 5,
+ mirror = true,
+ }
+ pickers
+ .new(opts, {
+ default_selection_index = cmake.project:current_build_index(),
+ prompt_title = "CMake Build Options",
+ finder = finders.new_table({
+ results = cmake.project:list_builds(),
+ entry_maker = cmake_make_entry.gen_from_configure(opts),
+ }),
+ sorter = conf.generic_sorter(opts),
+ previewer = previewers.build_previewer(),
+ attach_mappings = function(prompt_bufnr, map)
+ actions.select_default:replace(function()
+ actions.close(prompt_bufnr)
+ local selection = action_state.get_selected_entry()
+ cmake.project.current_config = selection.value
+ runner.start(selection.value.build_command)
+ end)
+ return true
+ end,
+ })
+ :find()
+end
+
+return M
diff --git a/lua/cmake/telescope/previewers.lua b/lua/cmake/telescope/previewers.lua
new file mode 100644
index 0000000..fee3d96
--- /dev/null
+++ b/lua/cmake/telescope/previewers.lua
@@ -0,0 +1,40 @@
+local previewers = require("telescope.previewers")
+local config = require("cmake.config")
+
+local M = {}
+
+M.configure_previewer = function(opts)
+ return previewers.new_buffer_previewer({
+ title = "Configure Details",
+
+ define_preview = function(self, entry)
+ if self.state.bufname then
+ return
+ end
+ local entries = {
+ "Command:",
+ config.cmake_path .. " " .. table.concat(entry.value.configure_args, " "),
+ }
+ vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, entries)
+ end,
+ })
+end
+
+M.build_previewer = function(opts)
+ return previewers.new_buffer_previewer({
+ title = "Build Details",
+
+ define_preview = function(self, entry)
+ if self.state.bufname then
+ return
+ end
+ local entries = {
+ "Command:",
+ config.cmake_path .. " " .. table.concat(entry.value.build_args, " "),
+ }
+ vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, entries)
+ end,
+ })
+end
+
+return M
diff --git a/lua/cmake/terminal.lua b/lua/cmake/terminal.lua
new file mode 100644
index 0000000..9581297
--- /dev/null
+++ b/lua/cmake/terminal.lua
@@ -0,0 +1,90 @@
+local Terminal = require("toggleterm.terminal").Terminal
+local ui = require("toggleterm.ui")
+local config = require("cmake.config")
+
+local M = {}
+
+local cmake
+local runnable
+
+--TODO: cmake must be an id, not terminal
+
+M.cmake_execute = function(command, opts)
+ opts = opts or {}
+ if cmake then
+ cmake:shutdown()
+ cmake = nil
+ end
+ local term_opts = {
+ direction = config.terminal.direction,
+ display_name = config.terminal.display_name,
+ hidden = config.terminal.hidden,
+ clear_env = config.terminal.clear_env,
+ cmd = command.cmd .. " " .. command.args,
+ -- env = command.env,
+ on_exit = function(t, pid, code, name)
+ if code == 0 then
+ command.after_success()
+ if config.terminal.close_on_exit == "success" then
+ t:close()
+ end
+ if config.notification.after == "success" or config.notification.after == true then
+ vim.notify(
+ vim.tbl_get(opts, "notify", "ok_message") or "CMake successfully completed",
+ vim.log.levels.INFO
+ )
+ end
+ elseif config.notification.after == "failure" or config.notification.after == true then
+ vim.notify(vim.inspect("failure "))
+ local msg = "CMake failed. Code " .. tostring(code)
+ local opt_msg = vim.tbl_get(opts, "notify", "err_message")
+ if type(opt_msg) == "string" then
+ msg = opt_msg
+ elseif type(opt_msg) == "function" then
+ msg = opt_msg(code)
+ end
+ vim.notify(msg, vim.log.levels.ERROR)
+ end
+ end,
+ on_open = function(t)
+ t:set_mode("n")
+ end,
+ }
+ term_opts.close_on_exit = type(config.terminal.close_on_exit) == "boolean" and config.terminal.close_on_exit
+ or false
+ cmake = Terminal:new(term_opts)
+ cmake:open()
+ if not config.terminal.focus and cmake:is_focused() then
+ ui.goto_previous()
+ ui.stopinsert()
+ end
+end
+
+M.cmake_toggle = function()
+ cmake:toggle()
+end
+
+M.target_execute = function(command, opts)
+ opts = opts or {}
+ local term_opts = {
+ direction = config.runner_terminal.direction,
+ close_on_exit = config.runner_terminal.close_on_exit,
+ hidden = config.runner_terminal.hidden,
+ clear_env = config.clear_env,
+ }
+ if not runnable then
+ runnable = Terminal:new(term_opts)
+ end
+ local cd = "cd " .. command.cwd
+ local cmd = "./" .. command.cmd
+ vim.notify(cd)
+ vim.notify(cmd)
+
+ if not runnable:is_open() then
+ runnable:open()
+ end
+ runnable:send(cd)
+ runnable:send(cmd)
+end
+
+return M
diff --git a/lua/cmake/utils.lua b/lua/cmake/utils.lua
new file mode 100644
index 0000000..1c68fae
--- /dev/null
+++ b/lua/cmake/utils.lua
@@ -0,0 +1,96 @@
+local config = require("cmake.config")
+local capabilities = require("cmake.capabilities")
+local scan = require("plenary.scandir")
+local Path = require("plenary.path")
+local uv = vim.loop
+
+local utils = {}
+
+utils.substitude = function(str, subs)
+ local ret = str
+ for k, v in pairs(subs) do
+ ret = ret:gsub(k, v)
+ end
+ return ret
+end
+
+function utils.touch_file(path, txt, flag, callback)
+ uv.fs_open(path, flag, 438, function(err, fd)
+ assert(not err, err)
+ assert(fd)
+ uv.fs_close(fd, function(c_err)
+ assert(not c_err, c_err)
+ if type(callback) == "function" then
+ callback()
+ end
+ end)
+ end)
+end
+
+function utils.file_exists(path, callback)
+ uv.fs_stat(path, function(err, _)
+ local exists
+ if err then
+ exists = false
+ else
+ exists = true
+ end
+ if type(callback) == "function" then
+ callback(exists)
+ end
+ end)
+end
+
+function utils.read_file(path, callback)
+ uv.fs_open(path, "r", 438, function(err, fd)
+ assert(not err, err)
+ assert(fd, fd)
+ uv.fs_fstat(fd, function(s_err, stat)
+ assert(not s_err, s_err)
+ assert(stat, stat)
+ uv.fs_read(fd, stat.size, 0, function(r_err, data)
+ assert(not r_err, r_err)
+ uv.fs_close(fd, function(c_err)
+ assert(not c_err, c_err)
+ callback(data)
+ end)
+ end)
+ end)
+ end)
+end
+
+function utils.write_file(path, txt, callback)
+ uv.fs_open(path, "w", 438, function(err, fd)
+ assert(not err, err)
+ assert(fd)
+ uv.fs_write(fd, txt, nil, function(w_err, _)
+ assert(not w_err, w_err)
+ uv.fs_close(fd, function(c_err)
+ assert(not c_err, c_err)
+ if type(callback) == "function" then
+ callback()
+ end
+ end)
+ end)
+ end)
+end
+
+--TODO: async mkdir -p
+
+function utils.symlink(src_path, dst_path, callback)
+ --TODO: replace to vim.fs.joinpath after nvim 0.10 release
+ local src = Path:new(src_path, "compile_commands.json")
+ if src:exists() then
+ vim.cmd(
+ 'silent exec "!'
+ .. config.cmake_path
+ .. " -E create_symlink "
+ .. src:normalize()
+ .. " "
+ .. Path:new(dst_path, "compile_commands.json"):normalize()
+ .. '"'
+ )
+ end
+end
+
+return utils
diff --git a/lua/cmake/variants.lua b/lua/cmake/variants.lua
new file mode 100644
index 0000000..50254c8
--- /dev/null
+++ b/lua/cmake/variants.lua
@@ -0,0 +1,157 @@
+local config = require("cmake.config")
+local utils = require("cmake.utils")
+
+local VariantConfig = {}
+
+VariantConfig.__index = VariantConfig
+
+local global_variant_subs = {
+ ["${workspaceFolder}"] = vim.loop.cwd(),
+ ["${userHome}"] = vim.loop.os_homedir(),
+}
+
+local _configure_args = function(obj, build_directory)
+ local args = {}
+ if obj.generator then
+ table.insert(args, "-G " .. '"' .. obj.generator .. '"')
+ end
+ table.insert(args, "-B" .. build_directory)
+ if obj.buildType then
+ table.insert(args, "-DCMAKE_BUILD_TYPE=" .. obj.buildType)
+ end
+ if obj.linkage and string.lower(obj.linkage) == "static" then
+ table.insert(args, "-DCMAKE_BUILD_SHARED_LIBS=OFF")
+ elseif obj.linkage and string.lower(obj.linkage) == "shared" then
+ table.insert(args, "-DCMAKE_BUILD_SHARED_LIBS=ON")
+ end
+ for k, v in pairs(obj.settings or {}) do
+ table.insert(args, "-D" .. k .. "=" .. v)
+ end
+ table.insert(args, "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON")
+ return args
+end
+
+local _configure_command = function(obj, configure_args)
+ local ret = {}
+ ret.cmd = config.cmake.cmake_path
+ ret.args = table.concat(configure_args, " ")
+ ret.env = vim.tbl_deep_extend("keep", obj.env, config.cmake.configure_environment, config.cmake.environment)
+ return ret
+end
+
+local _build_args = function(obj, build_directory)
+ local args = { "--build" }
+ table.insert(args, build_directory)
+ if #obj.buildArgs ~= 0 then
+ for _, v in ipairs(obj.buildArgs) do
+ table.insert(args, v)
+ end
+ elseif #config.cmake.build_args ~= 0 then
+ for _, v in ipairs(config.cmake.build_args) do
+ table.insert(args, v)
+ end
+ end
+ if #obj.buildToolArgs ~= 0 or #config.cmake.build_tool_args ~= 0 then
+ table.insert(args, "--")
+ if #obj.buildToolArgs ~= 0 then
+ for _, v in ipairs(obj.buildToolArgs) do
+ table.insert(args, v)
+ end
+ elseif #config.cmake.build_tool_args ~= 0 then
+ for _, v in ipairs(config.cmake.build_tool_args) do
+ table.insert(args, v)
+ end
+ end
+ end
+ return args
+end
+
+local _build_command = function(obj, build_args)
+ local ret = {}
+ ret.cmd = config.cmake.cmake_path
+ ret.args = table.concat(build_args, " ")
+ ret.env = vim.tbl_deep_extend("keep", obj.env, config.cmake.configure_environment, config.cmake.environment)
+ return ret
+end
+
+function VariantConfig:new(source)
+ local obj = {}
+ local subs = vim.tbl_deep_extend("keep", global_variant_subs, { ["${buildType}"] = source.buildType })
+
+ obj.name = source.short
+ obj.long_name = source.long
+ obj.directory = utils.substitude(config.cmake.build_directory, subs)
+ local configure_args = _configure_args(source, obj.directory)
+ obj.generate_command = _configure_command(source, configure_args)
+ local build_args = _build_args(source, obj.directory)
+ obj.build_options = {
+ {
+ name = source.short,
+ long_name = source.long,
+ command = _build_command(source, build_args),
+ },
+ }
+
+ setmetatable(obj, VariantConfig)
+ return obj
+end
+
+function VariantConfig.cartesian_product(sets)
+ -- vim.notify("cartesian_product", vim.log.levels.INFO)
+ local function collapse_result(res)
+ -- vim.notify("collapse_result", vim.log.levels.INFO)
+ local ret = {
+ short = {},
+ long = {},
+ buildType = nil,
+ linkage = nil,
+ generator = nil,
+ buildArgs = {},
+ buildToolArgs = {},
+ settings = {},
+ env = {},
+ }
+ local is_default = true
+ for _, v in ipairs(res) do
+ if not v.default then
+ is_default = false
+ end
+ ret.short[#ret.short + 1] = v.short
+ ret.long[#ret.long + 1] = v.long
+ ret.buildType = v.buildType or ret.buildType
+ ret.linkage = v.linkage or ret.linkage
+ ret.generator = v.generator or ret.generator
+ ret.buildArgs = v.buildArgs or ret.buildArgs
+ ret.buildToolArgs = v.buildToolArgs or ret.buildToolArgs
+ for sname, sval in pairs(v.settings or {}) do
+ ret.settings[sname] = sval
+ end
+ for ename, eres in pairs(v.env or {}) do
+ ret.env[ename] = eres
+ end
+ end
+ -- vim.notify(vim.inspect(ret), vim.log.levels.INFO)
+ return VariantConfig:new(ret), is_default
+ end
+ local result = {}
+ local set_count = #sets
+ local function descend(depth)
+ for k, v in pairs(sets[depth].choices) do
+ if sets[depth].default ~= k then
+ result.default = false
+ end
+ result[depth] = v
+ result[depth].default = (k == sets[depth].default)
+ if depth == set_count then
+ coroutine.yield(collapse_result(result))
+ else
+ descend(depth + 1)
+ end
+ end
+ end
+ return coroutine.wrap(function()
+ descend(1)
+ end)
+end
+
+return VariantConfig