lib/lua: refactor toLuaObject, now configurable

Heavily based on nixpkgs lib.generators.toPretty
This commit is contained in:
Matt Sturgeon 2024-06-26 15:06:56 +01:00
parent cd479ec0ef
commit aff12581d8
No known key found for this signature in database
GPG key ID: 4F91844CED1A8299
3 changed files with 194 additions and 62 deletions

View file

@ -26,7 +26,7 @@ rec {
}; };
vim-plugin = import ./vim-plugin.nix { inherit lib nixvimOptions nixvimUtils; }; vim-plugin = import ./vim-plugin.nix { inherit lib nixvimOptions nixvimUtils; };
inherit nixvimTypes; inherit nixvimTypes;
inherit (lua) toLuaObject; toLuaObject = lua.toLua;
} }
// nixvimUtils // nixvimUtils
// nixvimOptions // nixvimOptions

View file

@ -32,51 +32,193 @@ rec {
# and contain only letters, digits, and underscores. # and contain only letters, digits, and underscores.
isIdentifier = s: !(isKeyword s) && (match "[A-Za-z_][0-9A-Za-z_]*" s) == [ ]; isIdentifier = s: !(isKeyword s) && (match "[A-Za-z_][0-9A-Za-z_]*" s) == [ ];
# toLua' with default options, aliased as toLuaObject at the top-level
toLua = toLua' { };
# Black functional magic that converts a bunch of different Nix types to their # Black functional magic that converts a bunch of different Nix types to their
# lua equivalents! # lua equivalents!
toLuaObject = toLua' =
args: {
if builtins.isAttrs args then /**
if hasAttr "__raw" args then If this option is true, attrsets like { __pretty = fn; val = ; }
args.__raw will use fn to convert val to a lua representation.
else if hasAttr "__empty" args then */
"{ }" allowPrettyValues ? false,
else
"{" /**
+ (concatStringsSep ", " ( If this option is true, values like { __raw = "print('hi)"; }
mapAttrsToList ( will render as print('hi')
*/
allowRawValues ? true,
/**
If this option is true, attrsets like { "__rawKey__print('hi')" = "a"; }
will render as { [print('hi')] = "a"] }
*/
allowRawAttrKeys ? true,
/**
If this option is true, attrsets like { __unkeyed.1 = "a"; b = "b"; }
will render as { "a", b = "b" }
*/
allowUnkeyedAttrs ? true,
/**
If this option is true, attrsets like { a = "a"; b = "b"; }
will render as { a, b = "b" } instead of { ["a"] = "a", ["b"] = "b" }
*/
allowUnquotedAttrKeys ? true,
# TODO: smartQuoteStyle: use ' or " based on string content
# TODO: allowRawStrings: use [=[ ]=] strings for multiline strings
/**
If this option is true, attrsets like { a = null; b = ""; }
will render as { ["b"] = "" }
*/
removeNullAttrValues ? true,
/**
If this option is true, attrsets like { a = { }; b = [ ]; c = ""; }
will render as { ["c"] = "" }
*/
removeEmptyAttrValues ? true,
/**
If this option is true, attrsets like { a.__empty = null; }
will render as { ["a"] = { } }, ignoring removeEmptyAttrValues and removeNullAttrValues.
*/
allowExplicitEmpty ? true,
/**
If this option is true, attrsets like { __emptyString = "foo"; }
will render as { [""] = "foo" }.
This is deprecated: use an attrset like { "" = "foo"; } instead.
*/
allowLegacyEmptyStringAttr ? true,
/**
If this option is true, the output is indented with newlines for attribute sets and lists
*/
# TODO: make this true by default
multiline ? false,
/**
Initial indentation level
*/
indent ? "",
}:
let
# If any of these are options are set, we need to preprocess the value
needsPreprocessing = removeNullAttrValues || removeEmptyAttrValues || allowExplicitEmpty;
# Slight optimization: only preprocess if we actually need to
preprocessValue = value: if needsPreprocessing then removeEmptiesRecursive value else value;
# Recursively filters `value`, removing any empty/null attrs (as configured)
# Does not recurse into "special" attrs, such as `__raw`
removeEmptiesRecursive =
value:
if allowPrettyValues && value ? __pretty && value ? val then
value
else if allowRawValues && value ? __raw then
value
else if isList value then
map removeEmptiesRecursive value
else if isAttrs value then
concatMapAttrs (
n: v: n: v:
let if removeNullAttrValues && v == null then
keyString = { }
if n == "__emptyString" then else if removeEmptyAttrValues && (v == [ ] || v == { }) then
"['']" { }
else if hasPrefix "__rawKey__" n then else if allowExplicitEmpty && v ? __empty then
"[${removePrefix "__rawKey__" n}]" { ${n} = { }; }
else if isIdentifier n then else if isAttrs v then
n let
else v' = removeEmptiesRecursive v;
"[${toLuaObject n}]"; in
valueString = toLuaObject v; if v' == { } then { } else { ${n} = v'; }
in else
if hasPrefix "__unkeyed" n then valueString else "${keyString} = ${valueString}" { ${n} = v; }
) (filterAttrs (n: v: v != null && (toLuaObject v != "{}")) args) ) value
)) else
+ "}" value;
else if builtins.isList args then
"{" + concatMapStringsSep ", " toLuaObject args + "}" # Return the dict-style table key, formatted as per the config
else if builtins.isString args then toTableKey =
# This should be enough! s:
builtins.toJSON args if allowRawAttrKeys && hasPrefix "__rawKey__" s then
else if builtins.isPath args then "[${removePrefix "__rawKey__" s}]"
builtins.toJSON (toString args) else if allowUnquotedAttrKeys && isIdentifier s then
else if builtins.isBool args then s
"${boolToString args}" else if allowLegacyEmptyStringAttr && s == "__emptyString" then
else if builtins.isFloat args then trace ''nixvim(toLua): __emptyString is deprecated, just use an attribute named "".'' (
"${toString args}" toTableKey ""
else if builtins.isInt args then )
"${toString args}" else
else if (args == null) then "[${go "" s}]";
"nil"
else # The main recursive function implementing `toLua`:
""; # Visit a value and print it as lua, with the specified indentation.
# Recursively visits child values with increasing indentation.
go =
indent: v:
let
# Calculate the start/end padding, including any linebreaks,
# based on multiline config and current indentation.
introSpace = if multiline then "\n${indent} " else " ";
outroSpace = if multiline then "\n${indent}" else " ";
in
if v == null then
"nil"
else if isInt v then
toString v
# toString loses precision on floats, so we use toJSON instead.
# It can output an exponent form supported by lua.
else if isFloat v then
builtins.toJSON v
else if isBool v then
boolToString v
else if isPath v then
go indent (toString v)
else if isString v then
# TODO: support lua's escape sequences, literal string, and content-appropriate quote style
# See https://www.lua.org/pil/2.4.html
# and https://www.lua.org/manual/5.1/manual.html#2.1
# and https://github.com/NixOS/nixpkgs/blob/00ba4c2c35f5e450f28e13e931994c730df05563/lib/generators.nix#L351-L365
builtins.toJSON v
else if v == [ ] || v == { } then
"{ }"
else if isFunction v then
abort "nixvim(toLua): Unexpected function: " + generators.toPretty { } v
else if isDerivation v then
abort "nixvim(toLua): Unexpected derivation: " + generators.toPretty { } v
else if isList v then
"{" + introSpace + concatMapStringsSep ("," + introSpace) (go (indent + " ")) v + outroSpace + "}"
else if isAttrs v then
# apply pretty values if allowed
if allowPrettyValues && v ? __pretty && v ? val then
v.__pretty v.val
# apply raw values if allowed
else if allowRawValues && v ? __raw then
# TODO: apply indentation to multiline raw values
v.__raw
else
"{"
+ introSpace
+ concatStringsSep ("," + introSpace) (
mapAttrsToList (
name: value:
(if allowExplicitEmpty && hasPrefix "__unkeyed" name then "" else toTableKey name + " = ")
+ addErrorContext "while evaluating an attribute `${name}`" (go (indent + " ") value)
) v
)
+ outroSpace
+ "}"
else
abort "nixvim(toLua): should never happen (v = ${v})";
in
value: go indent (preprocessValue value);
} }

View file

@ -55,7 +55,7 @@ let
3 3
]; ];
}; };
expected = ''{foo = "bar", qux = {1, 2, 3}}''; expected = ''{ foo = "bar", qux = { 1, 2, 3 } }'';
}; };
testToLuaObjectRawLua = { testToLuaObjectRawLua = {
@ -68,7 +68,7 @@ let
"__unkeyed...." = "foo"; "__unkeyed...." = "foo";
bar = "baz"; bar = "baz";
}; };
expected = ''{"foo", bar = "baz"}''; expected = ''{ "foo", bar = "baz" }'';
}; };
testToLuaObjectNestedAttrs = { testToLuaObjectNestedAttrs = {
@ -81,7 +81,7 @@ let
}; };
}; };
}; };
expected = ''{a = {b = 1, c = 2, d = {e = 3}}}''; expected = ''{ a = { b = 1, c = 2, d = { e = 3 } } }'';
}; };
testToLuaObjectNestedList = { testToLuaObjectNestedList = {
@ -98,7 +98,7 @@ let
] ]
7 7
]; ];
expected = "{1, 2, {3, 4, {5, 6}}, 7}"; expected = "{ 1, 2, { 3, 4, { 5, 6 } }, 7 }";
}; };
testToLuaObjectNonStringPrims = { testToLuaObjectNonStringPrims = {
@ -109,7 +109,7 @@ let
d = false; d = false;
e = null; e = null;
}; };
expected = ''{a = 1.000000, b = 2, c = true, d = false}''; expected = ''{ a = 1.0, b = 2, c = true, d = false }'';
}; };
testToLuaObjectNilPrim = { testToLuaObjectNilPrim = {
@ -134,7 +134,7 @@ let
f = { }; f = { };
}; };
}; };
expected = ''{}''; expected = ''{ }'';
}; };
testToLuaObjectEmptyTable = { testToLuaObjectEmptyTable = {
@ -150,17 +150,7 @@ let
g = helpers.emptyTable; g = helpers.emptyTable;
}; };
}; };
expected = ''{c = { }, d = {g = { }}}''; expected = ''{ c = { }, d = { g = { } } }'';
};
testToLuaObjectQuotedKeys = {
expr = helpers.toLuaObject {
"1_a" = "a";
_b = "b";
c = "c";
d-d = "d";
};
expected = ''{["1_a"] = "a", _b = "b", c = "c", ["d-d"] = "d"}'';
}; };
testIsLuaKeyword = { testIsLuaKeyword = {