From aff12581d8cbeb27c58b5871ebe6c1405cabd4d9 Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Wed, 26 Jun 2024 15:06:56 +0100 Subject: [PATCH] lib/lua: refactor toLuaObject, now configurable Heavily based on nixpkgs lib.generators.toPretty --- lib/helpers.nix | 2 +- lib/to-lua.nix | 230 +++++++++++++++++++++++++++++++++++--------- tests/lib-tests.nix | 24 ++--- 3 files changed, 194 insertions(+), 62 deletions(-) diff --git a/lib/helpers.nix b/lib/helpers.nix index 3c610613..e03e11d7 100644 --- a/lib/helpers.nix +++ b/lib/helpers.nix @@ -26,7 +26,7 @@ rec { }; vim-plugin = import ./vim-plugin.nix { inherit lib nixvimOptions nixvimUtils; }; inherit nixvimTypes; - inherit (lua) toLuaObject; + toLuaObject = lua.toLua; } // nixvimUtils // nixvimOptions diff --git a/lib/to-lua.nix b/lib/to-lua.nix index d02ba768..3f789dfa 100644 --- a/lib/to-lua.nix +++ b/lib/to-lua.nix @@ -32,51 +32,193 @@ rec { # and contain only letters, digits, and underscores. 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 # lua equivalents! - toLuaObject = - args: - if builtins.isAttrs args then - if hasAttr "__raw" args then - args.__raw - else if hasAttr "__empty" args then - "{ }" - else - "{" - + (concatStringsSep ", " ( - mapAttrsToList ( + toLua' = + { + /** + If this option is true, attrsets like { __pretty = fn; val = …; } + will use fn to convert val to a lua representation. + */ + allowPrettyValues ? false, + + /** + If this option is true, values like { __raw = "print('hi)"; } + 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: - let - keyString = - if n == "__emptyString" then - "['']" - else if hasPrefix "__rawKey__" n then - "[${removePrefix "__rawKey__" n}]" - else if isIdentifier n then - n - else - "[${toLuaObject n}]"; - valueString = toLuaObject v; - in - if hasPrefix "__unkeyed" n then valueString else "${keyString} = ${valueString}" - ) (filterAttrs (n: v: v != null && (toLuaObject v != "{}")) args) - )) - + "}" - else if builtins.isList args then - "{" + concatMapStringsSep ", " toLuaObject args + "}" - else if builtins.isString args then - # This should be enough! - builtins.toJSON args - else if builtins.isPath args then - builtins.toJSON (toString args) - else if builtins.isBool args then - "${boolToString args}" - else if builtins.isFloat args then - "${toString args}" - else if builtins.isInt args then - "${toString args}" - else if (args == null) then - "nil" - else - ""; + if removeNullAttrValues && v == null then + { } + else if removeEmptyAttrValues && (v == [ ] || v == { }) then + { } + else if allowExplicitEmpty && v ? __empty then + { ${n} = { }; } + else if isAttrs v then + let + v' = removeEmptiesRecursive v; + in + if v' == { } then { } else { ${n} = v'; } + else + { ${n} = v; } + ) value + else + value; + + # Return the dict-style table key, formatted as per the config + toTableKey = + s: + if allowRawAttrKeys && hasPrefix "__rawKey__" s then + "[${removePrefix "__rawKey__" s}]" + else if allowUnquotedAttrKeys && isIdentifier s then + s + else if allowLegacyEmptyStringAttr && s == "__emptyString" then + trace ''nixvim(toLua): __emptyString is deprecated, just use an attribute named "".'' ( + toTableKey "" + ) + else + "[${go "" s}]"; + + # 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); } diff --git a/tests/lib-tests.nix b/tests/lib-tests.nix index e512661a..bda12598 100644 --- a/tests/lib-tests.nix +++ b/tests/lib-tests.nix @@ -55,7 +55,7 @@ let 3 ]; }; - expected = ''{foo = "bar", qux = {1, 2, 3}}''; + expected = ''{ foo = "bar", qux = { 1, 2, 3 } }''; }; testToLuaObjectRawLua = { @@ -68,7 +68,7 @@ let "__unkeyed...." = "foo"; bar = "baz"; }; - expected = ''{"foo", bar = "baz"}''; + expected = ''{ "foo", bar = "baz" }''; }; 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 = { @@ -98,7 +98,7 @@ let ] 7 ]; - expected = "{1, 2, {3, 4, {5, 6}}, 7}"; + expected = "{ 1, 2, { 3, 4, { 5, 6 } }, 7 }"; }; testToLuaObjectNonStringPrims = { @@ -109,7 +109,7 @@ let d = false; e = null; }; - expected = ''{a = 1.000000, b = 2, c = true, d = false}''; + expected = ''{ a = 1.0, b = 2, c = true, d = false }''; }; testToLuaObjectNilPrim = { @@ -134,7 +134,7 @@ let f = { }; }; }; - expected = ''{}''; + expected = ''{ }''; }; testToLuaObjectEmptyTable = { @@ -150,17 +150,7 @@ let g = helpers.emptyTable; }; }; - 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"}''; + expected = ''{ c = { }, d = { g = { } } }''; }; testIsLuaKeyword = {