diff --git a/home/features/neovim/config/lua/my/tempest.lua b/home/features/neovim/config/lua/my/tempest.lua index 18c6e72..bc930c9 100644 --- a/home/features/neovim/config/lua/my/tempest.lua +++ b/home/features/neovim/config/lua/my/tempest.lua @@ -118,6 +118,10 @@ local function recursive_assign(source, destination) end function M.configure(opts, context) + if type(opts) == "function" then + return M.configure(opts(context), context) + end + if type(opts.mk_context) == "function" then context = opts.mk_context(context) end @@ -223,5 +227,34 @@ function M.withSavedCursor(callback) vim.api.nvim_win_set_cursor(0, cursor) end -- }}} +-- {{{ Fixup lazy spec generated by nix +function M.prepareLazySpec(spec) + for _, module in ipairs(spec) do + if spec.tempest ~= nil then + spec.config = function(lazy, opts) + M.configure(spec.tempest, { lazy = lazy, opts = opts }) + end + end + + if spec.dependencies ~= nil then + spec.dependencies = spec.dependencies.lua + end + + if spec.keys ~= nil then + local keys = spec.keys + if spec.keys.mapping ~= nil then + keys = { keys } + end + + for _, key in ipairs(keys) do + key[1] = key.mapping + if key.mode ~= nil then + key.mode = H.string_chars(key.mode) + end + end + end + end +end +-- }}} return M diff --git a/modules/common/korora-lua.nix b/modules/common/korora-lua.nix new file mode 100644 index 0000000..205a0a9 --- /dev/null +++ b/modules/common/korora-lua.nix @@ -0,0 +1,292 @@ +{ lib, korora, ... }: +let + k = korora; + + # {{{ Lua encoders + # {{{ Helpers + helpers = rec { + xor = a: b: (a || b) && (!a || !b); + implies = a: b: !a || b; + hasProp = obj: p: (obj.${p} or null) != null; + propXor = a: b: obj: xor (hasProp obj a) (hasProp obj b); + propOnlyOne = props: obj: + 1 == lib.count (prop: obj ? prop); + propImplies = a: b: obj: implies (hasProp obj a) (hasProp obj b); + mkVerify = checks: obj: + let + results = lib.lists.map checks obj; + errors = lib.lists.filter (v: v != null) results; + in + if errors == [ ] + then null + else + let prettyErrors = + lib.lists.map (s: "\n- ${s}") errors; + in + "Multiple errors occured: ${prettyErrors}"; + + intersection = l: r: + k.typedef' + "${l.name} ∧ ${r.name}" + (helpers.mkVerify [ l r ]); + + dependentAttrsOf = + name: mkType: + let + typeError = name: v: "Expected type '${name}' but value '${toPretty v}' is of type '${typeOf v}'"; + addErrorContext = context: error: if error == null then null else "${context}: ${error}"; + + withErrorContext = addErrorContext "in ${name} value"; + in + k.typedef' name + (v: + if ! lib.isAttrs v then + typeError name v + else + withErrorContext + (mkVerify + (lib.mapAttrsToList (k: _: mkType k) v) + v)); + + mkRawLuaObject = chunks: + '' + { + ${lib.concatStringsSep "," (lib.filter (s: s != "") chunks)} + } + ''; + + mkAttrName = s: + let + # These list *are* incomplete + forbiddenChars = lib.stringToCharacters "<>[]{}()'\".,;"; + keywords = [ "if" "then" "else" "do" "for" "local" "" ]; + in + if lib.any (c: lib.hasInfix c s) forbiddenChars || lib.elem s keywords then + "[${m.string s}]" + else s; + + }; + # }}} + + # We provide a custom set of helpers for generating lua code for nix.enable + # + # An encoder is a function from some nix value to a string containing lua code. + # This object provides combinators for writing such encoders. + m = { + # {{{ General helpers + typed = type: toLua: type // { + unsafeToLua = toLua; + toLua = v: toLua (type.check v); + override = a: m.typed (type.override a) toLua; + }; + withDefault = type: default: type // { + default.value = default; + }; + typedWithDefault = type: toLua: default: + m.withDefault (m.typed type toLua) default; + unsafe = type: type // { toLua = type.unsafeToLua; }; + untyped = m.typed k.any; + untype = type: m.untyped type.toLua; + withName = name: type: type // { inherit name; }; # TODO: should we use override here? + # `const` is mostly useful together with `bind`. See the lua encoder for + # lazy modules for example usage. + const = code: m.untyped (_: code); + # Conceptually, this is the monadic bind operation for encoders. + # This implementation is isomoprhic to that of the reader monad in haskell. + bind = name: higherOrder: + m.typed + (k.typedef' name (v: (higherOrder v).verify v)) + (given: (higherOrder given).toLua given); + # This is probably the most useful combinnator defined in this entire object. + # Most of the combinators in the other categories are based on this. + conditional = predicate: caseTrue: caseFalse: + let base = m.bind + m.bind + "${caseTrue.name} ∨ ${caseFalse.name}" + (given: if predicate given then caseTrue else caseFalse); + in + if caseTrue ? default then + m.withDefault base caseTrue.default + else if caseFalse ? default then + m.withDefault base caseFalse.default + else + base; + try = caseTrue: caseFalse: + let base = m.bind + "${caseTrue.name} ∨ ${caseFalse.name}" + (given: if caseTrue.verify given == null then m.unsafe caseTrue else caseFalse); + in + if caseTrue ? default then + m.withDefault base caseTrue.default + else if caseFalse ? default then + m.withDefault base caseFalse.default + else + base; + oneOf = lib.foldr m.try + (m.bottom (v: "No variants matched for value ${builtins.toJSON v}")); + # This is simply left-composition of functions + cmap = f: t: + m.typed + (lib.typedef' t.name (v: t.verify (f v))) + (given: t.toLua (f given)); + # This is simply right-composition of functions + map = f: t: m.typed t (given: f (t.toLua given)); + filter = predicate: type: given: + m.conditional predicate type (m.untype m.nil); + # This is mostly useful for debugging + trace = message: m.cmap (f: lib.traceSeq message (lib.traceVal f)); + bottom = mkMessage: m.typed (m.typedef "⊥" mkMessage) lib.id; + # }}} + # {{{ Base types + string = m.typed k.string (given: ''"${lib.escape ["\"" "\\"] (toString given)}"''); + bool = m.typed k.bool (bool: if bool then "true" else "false"); + integer = m.typed k.int toString; + float = m.typed k.float toString; + number = m.typed k.number toString; + ignored = type: m.typed type (_: "nil"); + nil = m.typedWithDefault + (k.typedef "null" (v: v == null)) + (_: "nil") + null; + stringOr = m.try m.string; + boolOr = m.try m.bool; + numberOr = m.try m.number; + nullOr = m.try m.nil; + anything = m.withName "⊤" (m.oneOf [ + m.markedLuaCode # Lua code expressions have priority over attrsets + m.string + m.number + m.bool + m.null + (m.listOf m.anything) + (m.attrsetOf m.anything) + ]); + # }}} + # {{{ Lua code + identity = m.typed k.string lib.id; + markedLuaCode = + m.typed + (k.struct "marked lua code" { + value = k.string; + __luaEncoderTag = k.enum "lua encoder tag" [ "lua" ]; + }) + (obj: obj.value); + # This is the most rudimentary (and currently only) way of handling paths. + luaImport = tag: + m.typed (k.typedef "path" lib.isPath) + (path: "dofile(${m.string "${path}"}).${tag}"); + # Accepts both tagged and untagged strings of lua code. + luaString = m.try m.markedLuaCode m.identity; + # This simply combines the above combinators into one. + luaCode = tag: m.try (m.luaImport tag) m.luaString; + # }}} + # {{{ Operators + conjunction = left: right: given: + m.typed (helpers.intersection left right) ( + let + l = left.toLua given; + r = right.toLua given; + in + if l == "nil" then r + else if r == "nil" then l + else "${l} and ${r}" + ); + all = lib.foldr m.conjunction m.nil; + # Similar to `all` but takes in a list and + # treats every element as a condition. + allIndices = name: type: + m.bind name + (g: lib.pipe g [ + (lib.lists.imap0 + (i: _: + m.cmap + (builtins.elemAt i) + type)) + m.all + ]); + # }}} + # {{{ Lists + listOf = type: list: + m.typedWithDefault + (k.listOf type) + (helpers.mkRawLuaObject (lib.lists.map type.toLua list)) + [ ]; + listOfOr = type: m.try (m.listOf type); + # Returns nil when given empty lists + tryNonemptyList = type: + m.typedWithDefault + (k.listOf type) + (m.filter + (l: l != [ ]) + (m.listOf type)) + [ ]; + oneOrMany = type: m.listOfOr type type; + # Can encode: + # - zero values as nil + # - one value as itself + # - multiple values as a list + zeroOrMany = type: m.nullOr (m.oneOrMany type); + # Coerces non list values to lists of one element. + oneOrManyAsList = type: m.listOfOr type (m.map (e: [ e ]) type); + # Coerces lists of one element to said element. + listAsOneOrMany = type: + m.cmap + (l: if lib.length l == 1 then lib.head l else l) + (m.oneOrMany type); + # }}} + # {{{ Attrsets + attrsetOf = type: + m.typed (k.attrsOf type) + (object: + helpers.mkRawLuaObject (lib.mapAttrsToList + (name: value: + let result = type.toLua value; + in + lib.optionalString (result != "nil") + "${helpers.mkAttrName name} = ${result}" + ) + object + ) + ); + # This is the most general combinator provided in this section. + # + # We accept: + # - order of props that should be interpreted as list elements + # - spec of props that should be interpreted as list elements + # - record of props that should be interpreted as attribute props + attrset = name: listOrder: spec: attrset: + m.cmap + (given: lib.mapAttrs + (key: type: + if given ? ${key} then + given.${key} + else + type.default or null) + spec) + (m.typed (k.struct name spec) ( + let + listChunks = lib.lists.map + (attr: + let result = spec.${attr}.toLua (attrset.${attr} or null); + in + lib.optionalString (result != "nil") result + ) + listOrder; + objectChunks = lib.mapAttrsToList + (attr: type: + let result = type.toLua (attrset.${attr} or null); + in + lib.optionalString (!(lib.elem attr listOrder) && result != "nil") + "${helpers.mkAttrName attr} = ${result}" + ) + spec; + in + helpers.mkRawLuaObject (listChunks ++ objectChunks) + )); + withAttrsCheck = type: verify: + type.override { inherit verify; }; + # }}} + }; + # }}} +in +m // { inherit helpers; } diff --git a/modules/common/korora-neovim.nix b/modules/common/korora-neovim.nix new file mode 100644 index 0000000..6a07d68 --- /dev/null +++ b/modules/common/korora-neovim.nix @@ -0,0 +1,36 @@ +attrs@{ lib, ... }: +let + e = import ./korora-lua.nix attrs; + h = e.helpers; + m = { + lazyModule = e.withAttrsCheck + (e.attrset "lazy module" [ "package" ] { + package = e.nullOr e.string; + dir = e.nullOr e.type; + version = e.nullOr e.string; + tag = e.nullOr e.string; + name = e.nullOr e.string; + main = e.nullOr e.string; + lazy = e.nullOr e.bool; + dependencies = e.tryNonemptyList (e.stringOr m.lazyModule); + config = e.anything; + cond = e.nullOr (e.bind (value: e.all [ + # TODO: we want a zip here + (e.nullOr (e.luaCode "cond")) + ])); + init = e.nullOr (e.luaCode "init"); + event = e.zeroOrMany e.string; + cmd = e.zeroOrMany e.string; + ft = e.zeroOrMany e.string; + keys = e.listOf e.anything; + passthrough = e.anything; + opts = e.anything; + }) + (h.mkVerify [ + (h.propOnlyOne [ "dir" "package" ]) + (h.propImplies "tag" "package") + (h.propImplies "version" "package") + ]); + }; +in +m