From 8d05fcaaeb1442c709f148ff934d92900743f051 Mon Sep 17 00:00:00 2001
From: Daniil Rozanov <daniilrozzanov@gmail.com>
Date: Fri, 26 Apr 2024 00:50:04 +0300
Subject: feat: command to edit variants

If there is no cmake-variants.yaml file in current directory, command creates it with default variants
---
 lua/cmake/actions.lua   |   19 +
 lua/cmake/commands.lua  |    6 +-
 lua/cmake/config.lua    |   16 +-
 lua/cmake/constants.lua |    3 +
 lua/cmake/fileapi.lua   |    2 +
 lua/cmake/lazy.lua      |   21 +
 lua/cmake/lyaml.lua     | 1038 +++++++++++++++++++++++------------------------
 lua/cmake/project.lua   |   44 +-
 lua/cmake/terminal.lua  |    2 -
 lua/cmake/test.lua      |   11 +
 lua/cmake/variants.lua  |    3 -
 11 files changed, 601 insertions(+), 564 deletions(-)
 create mode 100644 lua/cmake/constants.lua
 create mode 100644 lua/cmake/lazy.lua
 create mode 100644 lua/cmake/test.lua

(limited to 'lua/cmake')

diff --git a/lua/cmake/actions.lua b/lua/cmake/actions.lua
index ff64f95..3c09d93 100644
--- a/lua/cmake/actions.lua
+++ b/lua/cmake/actions.lua
@@ -1,6 +1,8 @@
 local pr = require("cmake.project")
 local config = require("cmake.config")
 local t = require("cmake.terminal")
+local utils = require("cmake.utils")
+local constants = require("cmake.constants")
 local Path = require("plenary.path")
 
 local M = {}
@@ -121,4 +123,21 @@ M.toggle = function()
 	t.cmake_toggle()
 end
 
+M.edit_variants = function()
+	utils.file_exists(constants.variants_yaml_filename, function(variants_exists)
+		if variants_exists then
+			vim.schedule(function()
+				vim.cmd(string.format("e %s", constants.variants_yaml_filename))
+			end)
+		else
+			local default_yaml = require("cmake.lyaml").dump(config.cmake.variants)
+			utils.write_file(constants.variants_yaml_filename, default_yaml, function()
+				vim.schedule(function()
+					vim.cmd(string.format("e %s", constants.variants_yaml_filename))
+				end)
+			end)
+		end
+	end)
+end
+
 return M
diff --git a/lua/cmake/commands.lua b/lua/cmake/commands.lua
index f754b1c..c2f6e75 100644
--- a/lua/cmake/commands.lua
+++ b/lua/cmake/commands.lua
@@ -29,7 +29,11 @@ M.register_commands = function()
 
 	cmd("CMakeToggle", function()
 		require("cmake.actions").toggle()
-	end, { desc = "Toggle CMake terminal" })
+	end, { desc = "Toggle terminal with cmake command" })
+
+	cmd("CMakeEditVariants", function()
+		require("cmake.actions").edit_variants()
+	end, { desc = "Edit variants" })
 end
 
 return M
diff --git a/lua/cmake/config.lua b/lua/cmake/config.lua
index 0e3a641..5a4cb75 100644
--- a/lua/cmake/config.lua
+++ b/lua/cmake/config.lua
@@ -9,12 +9,14 @@ local default_config = {
 		build_tool_args = {},
 		generator = nil,
 		variants = {
-			{
+			buildType = {
 				default = "debug",
 				description = "Build type",
 				choices = {
-					debug = { short = "Debug", long = "Long debug", buildType = "Debug" },
-					release = { short = "Release", long = "Long release", buildType = "Release" },
+					debug = { short = "Debug", buildType = "Debug" },
+					release = { short = "Release", buildType = "Release" },
+					relWithDebInfo = { short = "Release with debug info", buildType = "RelWithDebInfo" },
+					minSizeRel = { short = "Minimal size releaze", buildType = "MinSizeRel" },
 				},
 			},
 		},
@@ -23,7 +25,7 @@ local default_config = {
 		source_directory = "${workspaceFolder}",
 	},
 	terminal = {
-		direction = "horizontal",
+		direction = "vertical",
 		display_name = "CMake",
 		close_on_exit = "success",
 		hidden = false,
@@ -31,7 +33,7 @@ local default_config = {
 		focus = false,
 	},
 	runner_terminal = {
-		direction = "horizontal",
+		direction = "vertical",
 		close_on_exit = false,
 		hidden = false,
 		clear_env = false,
@@ -41,8 +43,8 @@ local default_config = {
 		after = "success",
 	},
 	variants_display = {
-		short = { sep = " × " },
-		long = { sep = " ❄ " },
+		short = { sep = " × ", show = true },
+		long = { sep = " ❄ ", show = false },
 	},
 }
 
diff --git a/lua/cmake/constants.lua b/lua/cmake/constants.lua
new file mode 100644
index 0000000..fcbfea3
--- /dev/null
+++ b/lua/cmake/constants.lua
@@ -0,0 +1,3 @@
+return {
+	variants_yaml_filename = "cmake-variants.yaml",
+}
diff --git a/lua/cmake/fileapi.lua b/lua/cmake/fileapi.lua
index 2f61677..47a7459 100644
--- a/lua/cmake/fileapi.lua
+++ b/lua/cmake/fileapi.lua
@@ -47,6 +47,8 @@ function FileApi.read_reply(path, callback)
 						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)
+								--FIX: this loop does not read all files if codemodel contains many targets. This is because libuv (or some external settings) forbids to open files
+								-- in async mode more than some limit number. Seems like the solution is to queue these calls and limit max number for opened files per time
 								for _, target in ipairs(codemodel.configurations[1].targets) do
 									utils.read_file(
 										Path:new(reply_dir, target.jsonFile):absolute(),
diff --git a/lua/cmake/lazy.lua b/lua/cmake/lazy.lua
new file mode 100644
index 0000000..7669dcd
--- /dev/null
+++ b/lua/cmake/lazy.lua
@@ -0,0 +1,21 @@
+local lazy = {}
+
+--- Require on index.
+---
+--- Will only require the module after the first index of a module.
+--- Only works for modules that export a table.
+---@param require_path string
+---@return table
+lazy.require = function(require_path)
+	return setmetatable({}, {
+		__index = function(_, key)
+			return require(require_path)[key]
+		end,
+
+		__newindex = function(_, key, value)
+			require(require_path)[key] = value
+		end,
+	})
+end
+
+return lazy
diff --git a/lua/cmake/lyaml.lua b/lua/cmake/lyaml.lua
index 9555a75..3a17cc1 100644
--- a/lua/cmake/lyaml.lua
+++ b/lua/cmake/lyaml.lua
@@ -1,519 +1,495 @@
---[[
-(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.
---]]
+-- (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
+	indent = indent or 0
+	done = done or {}
+	if type(value) == "table" then
+		-- done[value] = true
+
+		local rep = "\n"
+		for key, val in pairs(value) do
+			local keyRep
+			if type(key) == "number" then
+				keyRep = "-"
+			else
+				keyRep = string.format("%s:", tostring(key))
+			end
+			local child_indent = indent + 2
+			rep = rep
+				.. string
+					.format("%s%s %s\n", string.rep(" ", indent), keyRep, table_print_value(val, child_indent, done))
+					:gsub("\n\n", "\n")
+		end
+
+		-- rep = rep .. string.rep(" ", indent) -- indent it
+
+		-- 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))
+	return vim.trim(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
+	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")
+	what = what or " "
+	return s:gsub("^[" .. what .. "]*(.-)[" .. what .. "]*$", "%1")
 end
 
 local push = function(stack, item)
-  stack[#stack + 1] = item
+	stack[#stack + 1] = item
 end
 
 local pop = function(stack)
-  local item = stack[#stack]
-  stack[#stack] = nil
-  return item
+	local item = stack[#stack]
+	stack[#stack] = nil
+	return item
 end
 
 local context = function(str)
-  if type(str) ~= "string" then
-    return ""
-  end
+	if type(str) ~= "string" then
+		return ""
+	end
 
-  str = str:sub(0, 25):gsub("\n", "\\n"):gsub('"', '\\"')
-  return ', near "' .. str .. '"'
+	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
+	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])"
+	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 ]+" },
+	{ "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
+	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]
+	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]
+	self.current = self.current + 1
+	return self.tokens[self.current]
 end
 
 Parser.advanceValue = function(self)
-  return self:advance()[2][1]
+	return self:advance()[2][1]
 end
 
 Parser.accept = function(self, type)
-  if self:peekType(type) then
-    return self:advance()
-  end
+	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))
+	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))
+	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
+	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
+	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" })
+	self:ignore({ "space" })
 end
 
 Parser.ignoreWhitespace = function(self)
-  self:ignore({ "space", "indent", "dedent" })
+	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
+	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()
+	self:accept("doc")
+	return self:parse()
 end
 
 Parser.inline = function(self)
-  local current = self:peek(0)
-  if not current then
-    return {}, 0
-  end
+	local current = self:peek(0)
+	if not current then
+		return {}, 0
+	end
 
-  local inline = {}
-  local i = 0
+	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
+	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
+	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]
+	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
+	return self:parent(level) and self:parent(level).token[1] == type
 end
 
 Parser.parseString = function(self)
-  if self:isInline() then
-    local result = self:advanceValue()
+	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
-
-    --[[
+		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
-    --[[
+		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
@@ -521,149 +497,149 @@ Parser.parseString = function(self)
         example
       c: 3
     --]]
-    return self:parseTextBlock("\n")
-  end
+		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
+	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
+	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
+	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
+	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()
+	local list = {}
+	while self:accept("-") do
+		self:ignoreSpace()
+		list[#list + 1] = self:parse()
 
-    self:ignoreSpace()
-  end
-  return list
+		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
+	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
+		self:ignoreSpace()
+		list[#list + 1] = self:parse()
+		self:ignoreSpace()
+		i = i + 1
+	end
 
-  return list
+	return list
 end
 
 Parser.parseTimestamp = function(self)
-  local capture = self:advance()[2]
+	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 })
+	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()
+	return Parser:new(exports.tokenize(str)):parse()
 end
 
 exports.dump = table_print
diff --git a/lua/cmake/project.lua b/lua/cmake/project.lua
index f98b82a..ad713e3 100644
--- a/lua/cmake/project.lua
+++ b/lua/cmake/project.lua
@@ -1,9 +1,9 @@
 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 constants = require("cmake.constants")
+local uv = vim.uv or vim.loop
 
 local Project = {}
 
@@ -52,7 +52,16 @@ 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
+	local list_variants = {}
+	for k, v in pairs(variants) do
+		table.insert(list_variants, v)
+		list_variants[#list_variants]._name = k
+	end
+	table.sort(list_variants, function(a, b)
+		vim.notify(a._name .. " " .. b._name)
+		return a._name < b._name
+	end)
+	for var, is_default in VariantConfig.cartesian_product(list_variants) do
 		var.current_build = 1
 		table.insert(configs, var)
 		current_config = not current_config and is_default and #configs or current_config
@@ -157,7 +166,7 @@ function Project.create_fileapi_query(opts, callback)
 		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)
+		--TODO: compare getmetatable(opts.config) with VariantConfig (and PresetsConfig in future)
 	elseif type(opts.config) == "table" then
 		path = opts.config.directory
 	else
@@ -176,22 +185,17 @@ 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,
-	})
+	local variants_path = vim.fs.joinpath(uv.cwd(), constants.variants_yaml_filename)
+	utils.file_exists(variants_path, function(variants_exists)
+		if variants_exists then
+			utils.read_file(variants_path, function(variants_data)
+				local yaml = require("cmake.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/terminal.lua b/lua/cmake/terminal.lua
index 9ba8cb1..358d731 100644
--- a/lua/cmake/terminal.lua
+++ b/lua/cmake/terminal.lua
@@ -35,7 +35,6 @@ M.cmake_execute = function(command, opts)
 					)
 				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
@@ -82,7 +81,6 @@ M.target_execute = function(command, opts)
 	if not runnable:is_open() then
 		runnable:open()
 	end
-	vim.notify(vim.inspect(command), vim.log.levels.INFO)
 	if command.cmd then
 		runnable:send(command.cmd, not config.runner_terminal.focus)
 	end
diff --git a/lua/cmake/test.lua b/lua/cmake/test.lua
new file mode 100644
index 0000000..d5fbab9
--- /dev/null
+++ b/lua/cmake/test.lua
@@ -0,0 +1,11 @@
+local lyaml = require("cmake.lyaml")
+lyaml.dump({ { foo = "bar" } })
+--> ---
+--> foo: bar
+--> ...
+
+lyaml.dump({ "one", "two" })
+--> --- one
+--> ...
+--> --- two
+--> ...
diff --git a/lua/cmake/variants.lua b/lua/cmake/variants.lua
index 50254c8..289d0e4 100644
--- a/lua/cmake/variants.lua
+++ b/lua/cmake/variants.lua
@@ -97,9 +97,7 @@ function VariantConfig:new(source)
 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 = {},
@@ -130,7 +128,6 @@ function VariantConfig.cartesian_product(sets)
 				ret.env[ename] = eres
 			end
 		end
-		-- vim.notify(vim.inspect(ret), vim.log.levels.INFO)
 		return VariantConfig:new(ret), is_default
 	end
 	local result = {}
-- 
cgit v1.2.3