diff options
Diffstat (limited to 'lua/cmake')
| -rw-r--r-- | lua/cmake/actions.lua | 111 | ||||
| -rw-r--r-- | lua/cmake/capabilities.lua | 63 | ||||
| -rw-r--r-- | lua/cmake/commands.lua | 31 | ||||
| -rw-r--r-- | lua/cmake/config.lua | 66 | ||||
| -rw-r--r-- | lua/cmake/fileapi.lua | 98 | ||||
| -rw-r--r-- | lua/cmake/init.lua | 19 | ||||
| -rw-r--r-- | lua/cmake/lyaml.lua | 671 | ||||
| -rw-r--r-- | lua/cmake/project.lua | 197 | ||||
| -rw-r--r-- | lua/cmake/telescope/make_entry.lua | 57 | ||||
| -rw-r--r-- | lua/cmake/telescope/pickers.lua | 102 | ||||
| -rw-r--r-- | lua/cmake/telescope/previewers.lua | 40 | ||||
| -rw-r--r-- | lua/cmake/terminal.lua | 90 | ||||
| -rw-r--r-- | lua/cmake/utils.lua | 96 | ||||
| -rw-r--r-- | lua/cmake/variants.lua | 157 | 
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  | 
