diff --git a/docs/default.nix b/docs/default.nix index bbb94d88cc..194c74c62b 100644 --- a/docs/default.nix +++ b/docs/default.nix @@ -1,6 +1,7 @@ { helpers, system, + nixvim, nixpkgs, nuschtosSearch, }: @@ -107,7 +108,14 @@ lib.fix ( gfm-alerts-to-admonitions = pkgs.python3.pkgs.callPackage ./gfm-alerts-to-admonitions { }; - man-docs = pkgs.callPackage ./man { inherit options-json; }; + man-docs = pkgs.callPackage ./man { + inherit options-json; + inherit (self) lib-docs; + }; + + lib-docs = pkgs.callPackage ./lib { + inherit nixvim lib; + }; } // lib.optionalAttrs (!pkgs.stdenv.isDarwin) { # NuschtOS/search does not seem to work on darwin @@ -122,7 +130,7 @@ lib.fix ( # > sandbox-exec: pattern serialization length 69298 exceeds maximum (65535) docs = pkgs.callPackage ./mdbook { inherit evaledModules transformOptions; - inherit (self) search; + inherit (self) search lib-docs; }; } ) diff --git a/docs/lib/default.nix b/docs/lib/default.nix new file mode 100644 index 0000000000..fb37706f0d --- /dev/null +++ b/docs/lib/default.nix @@ -0,0 +1,178 @@ +# Generates the documentation for library functions using nixdoc. +# See https://github.com/nix-community/nixdoc +{ + lib, + runCommand, + writers, + nixdoc, + nixvim, + pageSpecs ? import ./pages.nix, +}: + +let + # Some pages are just menu entries, others have an actual markdown page that + # needs rendering. + shouldRenderPage = page: page ? file || page ? markdown; + + # Normalise a page node, recursively normalise its children + elaboratePage = + loc: + { + title ? "", + markdown ? null, + file ? null, + pages ? { }, + }@page: + { + name = lib.attrsets.showAttrPath loc; + loc = lib.throwIfNot ( + builtins.head loc == "lib" + ) "All pages must be within `lib`, unexpected root `${builtins.head loc}`" (builtins.tail loc); + } + // lib.optionalAttrs (shouldRenderPage page) { + inherit + file + title + ; + markdown = + if builtins.isString markdown then + builtins.toFile "${lib.strings.replaceStrings [ "/" "-" ] (lib.lists.last loc)}.md" markdown + else + markdown; + outFile = lib.strings.concatStringsSep "/" (loc ++ [ "index.md" ]); + } + // lib.optionalAttrs (page ? pages) { + pages = elaboratePages loc pages; + }; + + # Recursively normalise page nodes + elaboratePages = prefix: builtins.mapAttrs (name: elaboratePage (prefix ++ [ name ])); + + # Collect all page nodes into a list of page entries + collectPages = + pages: + builtins.concatMap ( + page: + [ (builtins.removeAttrs page [ "pages" ]) ] + ++ lib.optionals (page ? pages) (collectPages page.pages) + ) (builtins.attrValues pages); + + # Normalised page specs + elaboratedPageSpecs = elaboratePages [ ] pageSpecs; + pageList = collectPages elaboratedPageSpecs; + pagesToRender = builtins.filter (page: page ? outFile) pageList; + pagesWithFunctions = builtins.filter (page: page.file or null != null) pageList; +in + +runCommand "nixvim-lib-docs" + { + nativeBuildInputs = [ + nixdoc + ]; + + locations = writers.writeJSON "locations.json" ( + import ./function-locations.nix { + inherit lib; + rootPath = nixvim; + functionSet = lib.extend nixvim.lib.overlay; + pathsToScan = builtins.catAttrs "loc" pagesWithFunctions; + revision = nixvim.rev or "main"; + } + ); + + passthru.menu = import ./menu.nix { + inherit lib; + pageSpecs = elaboratedPageSpecs; + }; + + passthru.pages = builtins.listToAttrs ( + builtins.map ( + { name, outFile, ... }: + { + inherit name; + value = outFile; + } + ) pagesToRender + ); + } + '' + function docgen { + md_file="$1" + in_file="$2" + name="$3" + out_file="$out/$4" + title="$5" + + if [[ -z "$in_file" ]]; then + if [[ -z "$md_file" ]]; then + >&2 echo "No markdown or nix file for $name" + exit 1 + fi + elif [[ -f "$in_file/default.nix" ]]; then + in_file+="/default.nix" + elif [[ ! -f "$in_file" ]]; then + >&2 echo "File not found: $in_file" + exit 1 + fi + + if [[ -n "$in_file" ]]; then + nixdoc \ + --file "$in_file" \ + --locs "$locations" \ + --category "$name" \ + --description "REMOVED BY TAIL" \ + --prefix "" \ + --anchor-prefix "" \ + | tail --lines +2 \ + > functions.md + fi + + default_heading="# $name" + if [[ -n "$title" ]]; then + default_heading+=": $title" + fi + + print_heading=true + if [[ -f "$md_file" ]] && [[ "$(head --lines 1 "$md_file")" == '# '* ]]; then + >&2 echo "NOTE: markdown file for $name starts with a

heading. Skipping default heading \"$default_heading\"." + >&2 echo " Found \"$(head --lines 1 "$md_file")\" in: $md_file" + print_heading=false + fi + + mkdir -p $(dirname "$out_file") + ( + if [[ "$print_heading" = true ]]; then + echo "$default_heading" + echo + fi + if [[ -f "$md_file" ]]; then + cat "$md_file" + echo + fi + if [[ -f functions.md ]]; then + cat functions.md + fi + ) > "$out_file" + } + + mkdir -p "$out" + + ${lib.concatMapStringsSep "\n" ( + { + name, + file, + markdown, + outFile, + title ? "", + ... + }: + lib.escapeShellArgs [ + "docgen" + "${lib.optionalString (markdown != null) markdown}" # md_file + "${lib.optionalString (file != null) file}" # in_file + name # name + outFile # out_file + title # title + ] + ) pagesToRender} + '' diff --git a/docs/lib/function-locations.nix b/docs/lib/function-locations.nix new file mode 100644 index 0000000000..e232d51496 --- /dev/null +++ b/docs/lib/function-locations.nix @@ -0,0 +1,85 @@ +# Generates an attrset of "function name" → "markdown location", +# for use with nixdoc's `--locs` option. +# +# { +# "lib.nixvim.foo.bar" = "[lib/foo.nix:123](https://github.com/nix-community/nixvim/blob/«rev»/lib/foo.nix#L123) in ``"; +# } +{ + rootPath, + lib, + functionSet, + pathsToScan, + revision, + functionSetName ? "lib", + url ? "https://github.com/nix-community/nixvim/blob", +}: +let + rootPathString = toString rootPath; + urlPrefix = "${url}/${revision}"; + + sanitizeId = builtins.replaceStrings [ "'" ] [ "-prime" ]; + + # Like `isAttrs`, but returns `false` if `v` throws + tryIsAttrs = v: (builtins.tryEval (builtins.isAttrs v)).value; + + # Collect position entries from an attrset + # `prefix` is used in the human-readable name, + # and for determining whether to recurse into attrs + collectPositionEntriesInSet = + prefix: set: + builtins.concatMap ( + name: + [ + { + name = lib.showAttrPath ( + builtins.concatMap lib.toList [ + functionSetName + prefix + name + ] + ); + location = builtins.unsafeGetAttrPos name set; + } + ] + ++ lib.optionals (prefix == [ ] && tryIsAttrs set.${name}) ( + collectPositionEntriesInSet (prefix ++ [ name ]) set.${name} + ) + ) (builtins.attrNames set); + + # Collect position entries from each `pathsToScan` in `set` + collectPositionEntriesFromPaths = + set: + builtins.concatMap (loc: collectPositionEntriesInSet loc (lib.getAttrFromPath loc set)) pathsToScan; + + # Remove the tree root (usually the top-level store path) + removeNixvimPrefix = lib.flip lib.pipe [ + (lib.strings.removePrefix rootPathString) + (lib.strings.removePrefix "/") + ]; + + # Create a name-value-pair for use with `listToAttrs` + entryToNameValuePair = + { name, location }: + { + name = sanitizeId name; + value = + let + file = removeNixvimPrefix location.file; + line = builtins.toString location.line; + text = "${file}:${line}"; + target = "${urlPrefix}/${file}#L${line}"; + in + "[${text}](${target}) in ``"; + }; +in +lib.pipe functionSet [ + # Get the entries + collectPositionEntriesFromPaths + # Only include entries that have a location + (builtins.filter (entry: entry.location != null)) + # No need to include out-of-tree entries + (builtins.filter (entry: lib.strings.hasPrefix rootPathString entry.location.file)) + # Convert entries to attrset + (builtins.map entryToNameValuePair) + builtins.listToAttrs +] diff --git a/docs/user-guide/helpers.md b/docs/lib/index.md similarity index 52% rename from docs/user-guide/helpers.md rename to docs/lib/index.md index 37d076f2fc..e52f98a6ce 100644 --- a/docs/user-guide/helpers.md +++ b/docs/lib/index.md @@ -1,5 +1,3 @@ -# Helpers - ## Accessing Nixvim's functions If Nixvim is built using the standalone method, you can access our "helpers" as part of the `lib` module arg: @@ -67,47 +65,3 @@ This can be achieved using the lib overlay, available via the `.lib.over }; } ``` - -## Common helper functions - -A certain number of helpers are defined that can be useful: - -- `helpers.emptyTable`: An empty lua table `{}` that will be included in the final lua configuration. - This is equivalent to `{__empty = {};}`. This form can allow to do `option.__empty = {}`. - -- `helpers.mkRaw str`: Write the string `str` as raw lua in the final lua configuration. - This is equivalent to `{__raw = "lua code";}`. This form can allow to do `option.__raw = "lua code"`. - -- `helpers.toLuaObject obj`: Create a string representation of the Nix object. Useful to define your own plugins. - -- `helpers.listToUnkeyedAttrs list`: Transforms a list to an "unkeyed" attribute set. - - This allows to define mixed table/list in lua: - - ```nix - (listToUnkeyedAttrs ["a" "b"]) // {foo = "bar";} - ``` - - Resulting in the following lua: - - ```lua - {"a", "b", [foo] = "bar"} - ``` - -- `helpers.enableExceptInTests`: Evaluates to `true`, except in `mkTestDerivationFromNixvimModule` - where it evaluates to `false`. This allows to skip instantiating plugins that can't be run in tests. - -- `helpers.toRawKeys attrs`: Convert the keys of the given `attrs` to raw lua. - ```nix - toRawKeys {foo = 1; bar = "hi";} - ``` - will translate in lua to: - ```lua - {[foo] = 1, [bar] = 2,} - ``` - Otherwise, the keys of a regular `attrs` will be interpreted as lua string: - ```lua - {['foo'] = 1, ['bar'] = 2,} - -- which is the same as - {foo = 1, bar = 2,} - ``` diff --git a/docs/lib/menu.nix b/docs/lib/menu.nix new file mode 100644 index 0000000000..87feb9c9cd --- /dev/null +++ b/docs/lib/menu.nix @@ -0,0 +1,31 @@ +{ + lib, + pageSpecs, + indentSize ? " ", +}: +let + pageToLines = + indent: parentName: + { + name, + outFile ? "", + pages ? { }, + ... + }: + let + menuName = lib.strings.removePrefix (parentName + ".") name; + children = builtins.attrValues pages; + # Only add node to the menu if it has content or multiple children + useNodeInMenu = outFile != "" || builtins.length children > 1; + parentOfChildren = if useNodeInMenu then name else parentName; + in + lib.optional useNodeInMenu "${indent}- [${menuName}](${outFile})" + ++ lib.optionals (children != [ ]) ( + builtins.concatMap (pageToLines (indent + indentSize) parentOfChildren) children + ); +in +lib.pipe pageSpecs [ + builtins.attrValues + (builtins.concatMap (pageToLines "" "")) + lib.concatLines +] diff --git a/docs/lib/pages.nix b/docs/lib/pages.nix new file mode 100644 index 0000000000..f519339e38 --- /dev/null +++ b/docs/lib/pages.nix @@ -0,0 +1,24 @@ +# This file contains a list of function reference docs pages that should be generated by `nixdoc` +# +# Note: `nixdoc` uses `rnix` to parse the input file, so the file must return a fairly simple attrset. +# If there is an issue parsing the file, the resulting markdown will not contain any function docs. + +{ + lib.pages = { + nixvim = { + title = "Nixvim's functions"; + markdown = ./index.md; + + pages = { + utils = { + file = ../../lib/utils.nix; + title = "utility functions"; + }; + lua = { + file = ../../lib/to-lua.nix; + title = "lua functions"; + }; + }; + }; + }; +} diff --git a/docs/man/default.nix b/docs/man/default.nix index c60743e134..7524370039 100644 --- a/docs/man/default.nix +++ b/docs/man/default.nix @@ -1,12 +1,17 @@ { lib, options-json, + lib-docs, runCommand, installShellFiles, nixos-render-docs, pandoc, }: let + markdownSections = [ + ../user-guide/faq.md + ../user-guide/config-examples.md + ] ++ lib.mapAttrsToList (name: file: "${lib-docs}/${file}") lib-docs.pages; manHeader = runCommand "nixvim-general-doc-manpage" { @@ -22,11 +27,7 @@ let ( cat ${./nixvim-header-start.5} - ${lib.concatMapStringsSep "\n" (file: "mkMDSection ${file}") [ - ../user-guide/helpers.md - ../user-guide/faq.md - ../user-guide/config-examples.md - ]} + ${lib.concatMapStringsSep "\n" (file: "mkMDSection ${file}") markdownSections} cat ${./nixvim-header-end.5} ) >$out/nixvim-header.5 diff --git a/docs/mdbook/SUMMARY.md b/docs/mdbook/SUMMARY.md index d08c19fd7d..88d01d8df3 100644 --- a/docs/mdbook/SUMMARY.md +++ b/docs/mdbook/SUMMARY.md @@ -5,11 +5,14 @@ # User guide - [Installation](./user-guide/install.md) -- [Helpers](./user-guide/helpers.md) - [FAQ](./user-guide/faq.md) - [Configuration examples](./user-guide/config-examples.md) - [Lazy Loading](./user-guide/lazy-loading.md) +# Functions + +@FUNCTIONS_MENU@ + # Platforms - [Nixvim Platforms](./platforms/index.md) diff --git a/docs/mdbook/book.toml b/docs/mdbook/book.toml index 2e11c8249c..a74ba126de 100644 --- a/docs/mdbook/book.toml +++ b/docs/mdbook/book.toml @@ -10,6 +10,11 @@ site-url = "@SITE_URL@" additional-js = ["theme/pagetoc.js"] additional-css = ["theme/pagetoc.css"] +# Redirect targets must be relative to their origin; +# absolute paths can break with different baseHrefs. +[output.html.redirect] +"/user-guide/helpers.html" = "../lib/index.html" # Since #3049 + [output.html.fold] enable = true level = 0 diff --git a/docs/mdbook/default.nix b/docs/mdbook/default.nix index a75c0b5e11..d8e779704e 100644 --- a/docs/mdbook/default.nix +++ b/docs/mdbook/default.nix @@ -7,6 +7,7 @@ nixosOptionsDoc, transformOptions, search, + lib-docs, # The root directory of the site baseHref ? "/", # A list of all available docs that should be linked to @@ -359,6 +360,13 @@ pkgs.stdenv.mkDerivation (finalAttrs: { # Copy the contributing file cp $contributing ./CONTRIBUTING.md + # Symlink the function docs + for path in ${lib-docs}/* + do + echo "symlinking \"$path\" to \"$(basename "$path")\"" + ln -s "$path" $(basename "$path") + done + # Copy the generated MD docs into the build directory bash -e ${finalAttrs.passthru.copy-docs} @@ -375,6 +383,7 @@ pkgs.stdenv.mkDerivation (finalAttrs: { # Patch SUMMARY.md - which defiens mdBook's table of contents substituteInPlace ./SUMMARY.md \ + --replace-fail "@FUNCTIONS_MENU@" "$functionsSummary" \ --replace-fail "@PLATFORM_OPTIONS@" "$wrapperOptionsSummary" \ --replace-fail "@NIXVIM_OPTIONS@" "$nixvimOptionsSummary" @@ -401,6 +410,8 @@ pkgs.stdenv.mkDerivation (finalAttrs: { wrapperOptionsFiles ; + functionsSummary = lib-docs.menu; + passthru = { fix-links = callPackage ../fix-links { # FIXME: determine values from availableVersions & baseHref diff --git a/flake/packages.nix b/flake/packages.nix index a9ba410a5d..03451350c1 100644 --- a/flake/packages.nix +++ b/flake/packages.nix @@ -1,4 +1,5 @@ { + self, inputs, helpers, ... @@ -12,6 +13,7 @@ }: { packages = import ../docs { + nixvim = self; inherit helpers; inherit system; inherit (inputs) nixpkgs; diff --git a/lib/default.nix b/lib/default.nix index 51dc36c889..2b86d67a2b 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -24,7 +24,7 @@ lib.makeExtensible ( modules = call ./modules.nix { inherit flake; }; options = call ./options.nix { }; plugins = call ./plugins { }; - utils = call ./utils.nix { inherit _nixvimTests; }; + utils = call ./utils.nix { inherit _nixvimTests; } // call ./utils.internal.nix { }; # Top-level helper aliases: # TODO: deprecate some aliases @@ -103,7 +103,7 @@ lib.makeExtensible ( wrapVimscriptForLua ; - toLuaObject = self.lua.toLua; + inherit (self.lua) toLuaObject; mkLuaInline = self.lua.mkInline; # TODO: Removed 2024-12-21 diff --git a/lib/modules.nix b/lib/modules.nix index 5bcba06146..5d73b09e08 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -27,7 +27,7 @@ in assert lib.assertMsg (extraSpecialArgs ? lib -> extraSpecialArgs.lib ? nixvim) '' Nixvim requires a lib that includes some custom extensions, however the `lib` from `specialArgs` does not have a `nixvim` attr. Remove `lib` from nixvim's `specialArgs` or ensure you apply nixvim's extensions to your `lib`. - See https://nix-community.github.io/nixvim/user-guide/helpers.html#using-a-custom-lib-with-nixvim''; + See https://nix-community.github.io/nixvim/lib/index.html#using-a-custom-lib-with-nixvim''; assert lib.assertMsg (system != null -> lib.isString system) '' When `system` is supplied to `evalNixvim`, it must be a string. To define a more complex system, please use nixvim's `nixpkgs.hostPlatform` option.''; diff --git a/lib/to-lua.nix b/lib/to-lua.nix index 4c4c7434df..3319a0541c 100644 --- a/lib/to-lua.nix +++ b/lib/to-lua.nix @@ -38,6 +38,21 @@ rec { # Whether the value is a lua-inline type isInline = v: v._type or null == "lua-inline"; + /** + Serialise a nix value as a lua object. + + Useful for defining your own plugins or structured config. + + # Type + + ``` + toLuaObject :: Any -> String + ``` + */ + toLuaObject = + # toLua' with backwards-compatible options + toLua' { }; + # toLua' with default options, aliased as toLuaObject at the top-level toLua = toLua' { }; diff --git a/lib/utils.internal.nix b/lib/utils.internal.nix new file mode 100644 index 0000000000..4940e2066d --- /dev/null +++ b/lib/utils.internal.nix @@ -0,0 +1,348 @@ +{ lib }: +rec { + # Whether a string contains something other than whitespaces + hasContent = str: builtins.match "[[:space:]]*" str == null; + + # Concatenate a list of strings, adding a newline at the end of each one, + # but skipping strings containing only whitespace characters + concatNonEmptyLines = lines: lib.concatLines (builtins.filter hasContent lines); + + /** + Add a prefix to the keys of an attrs. + + # Example + ```nix + applyPrefixToAttrs "prefix_" { foo = 1; bar = 2; } + => { prefix_foo = 1; prefix_bar = 2; } + ``` + + # Type + + ``` + applyPrefixToAttrs :: String -> AttrSet -> AttrSet + ``` + */ + applyPrefixToAttrs = prefix: lib.mapAttrs' (n: lib.nameValuePair (prefix + n)); + + /* + Convert a string from camelCase to snake_case + Type: string -> string + */ + toSnakeCase = + let + splitByWords = builtins.split "([A-Z])"; + processWord = s: if lib.isString s then s else "_" + lib.toLower (lib.elemAt s 0); + in + string: + let + words = splitByWords string; + in + lib.concatStrings (map processWord words); + + /** + Those helpers control the lua sections split in `pre, content, post` + */ + mkBeforeSection = lib.mkOrder 300; + mkAfterSection = lib.mkOrder 2000; + + /** + Capitalize a string by making the first character uppercase. + + # Example + + ```nix + upperFirstChar "hello, world!" + => "Hello, world!" + ``` + + # Type + + ``` + upperFirstChar :: String -> String + ``` + */ + upperFirstChar = + s: + let + first = lib.substring 0 1 s; + rest = lib.substring 1 (lib.stringLength s) s; + result = (lib.toUpper first) + rest; + in + lib.optionalString (s != "") result; + + mkIfNonNull' = x: y: (lib.mkIf (x != null) y); + + mkIfNonNull = x: (mkIfNonNull' x x); + + ifNonNull' = x: y: if (x == null) then null else y; + + /** + Convert the given string into a literalExpression mkRaw. + + For use in option documentation, such as examples and defaults. + + # Example + + ```nix + literalLua "print('hi')" + => literalExpression ''lib.nixvim.mkRaw "print('hi')"'' + => { + _type = "literalExpression"; + text = ''lib.nixvim.mkRaw "print('hi')"''; + } + ``` + + # Type + + ``` + literalLua :: String -> AttrSet + ``` + */ + literalLua = + r: + let + # Pass the value through mkRaw for validation + raw = lib.nixvim.mkRaw r; + # TODO: consider switching to lib.generators.mkLuaInline ? + exp = "lib.nixvim.mkRaw " + lib.generators.toPretty { } raw.__raw; + in + lib.literalExpression exp; + + __messagePrefix = scope: "Nixvim (${scope}):"; + + /** + Process one or several assertions by prepending the common 'Nixvim (): ' prefix to messages. + The second argument can either be a list of assertions or a single one. + + # Example + + ```nix + assertions = mkAssertions "plugins.foo" { + assertion = plugins.foo.settings.barIntegration && (!plugins.bar.enable); + message = "`barIntegration` is enabled but the `bar` plugin is not." + } + ``` + + # Type + + ``` + mkAssertions :: String -> List -> List + ``` + */ + mkAssertions = + scope: assertions: + let + prefix = __messagePrefix scope; + processAssertion = a: { + inherit (a) assertion; + message = "${prefix} ${lib.trim a.message}"; + }; + in + builtins.map processAssertion (lib.toList assertions); + + /** + Convert one or several conditional warnings to a final warning list. + The second argument can either be a list of _conditional warnings_ or a single one. + + # Example + + ```nix + warnings = mkWarnings "plugins.foo" { + when = plugins.foo.settings.barIntegration && (!plugins.bar.enable); + message = "`barIntegration` is enabled but the `bar` plugin is not." + } + ``` + + # Type + + ``` + mkWarnings :: String -> List -> List + ``` + */ + mkWarnings = + scope: warnings: + let + prefix = __messagePrefix scope; + processWarning = + warning: lib.optional (warning.when or true) "${prefix} ${lib.trim (warning.message or warning)}"; + in + builtins.concatMap processWarning (lib.toList warnings); + + /** + Convert the given string into a `__pretty` printed mkRaw expression. + + For use in nested values that will be printed by `lib.generators.toPretty`, + or `lib.options.renderOptionValue`. + + # Example + + ```nix + nestedLiteralLua "print('hi')" + => nestedLiteral (literalLua "print('hi')") + => { + __pretty = lib.getAttr "text"; + val = literalLua "print('hi')"; + } + ``` + + # Type + + ``` + nestedLiteralLua :: String -> AttrSet + ``` + */ + nestedLiteralLua = r: nestedLiteral (literalLua r); + + /** + Convert the given string or literalExpression into a `__pretty` printed expression. + + For use in nested values that will be printed by `lib.generators.toPretty`, + or `lib.options.renderOptionValue`. + + # Examples + + ```nix + nestedLiteral "example" + => { + __pretty = lib.getAttr "text"; + val = literalExpression "example"; + } + ``` + + ```nix + nestedLiteral (literalExpression ''"hello, world"'') + => { + __pretty = lib.getAttr "text"; + val = literalExpression ''"hello, world"''; + } + ``` + + # Type + + ``` + nestedLiteral :: (String | literalExpression) -> AttrSet + ``` + */ + nestedLiteral = val: { + __pretty = lib.getAttr "text"; + val = if val._type or null == "literalExpression" then val else lib.literalExpression val; + }; + + wrapDo = string: '' + do + ${string} + end + ''; + + /** + Convert the given String to a Lua [long literal]. + For example, you could use this to safely pass a Vimscript string to the + `vim.cmd` function. + + [long literal]: https://www.lua.org/manual/5.4/manual.html#3.1 + + # Examples + + ```nix + nix-repl> toLuaLongLiteral "simple" + "[[simple]]" + ``` + + ```nix + nix-repl> toLuaLongLiteral "]]" + "[=[]]]=]" + ``` + + # Type + + ``` + toLuaLongLiteral :: String -> String + ``` + */ + toLuaLongLiteral = + string: + let + findTokens = + depth: + let + infix = lib.strings.replicate depth "="; + tokens.open = "[${infix}["; + tokens.close = "]${infix}]"; + in + if lib.hasInfix tokens.close string then findTokens (depth + 1) else tokens; + + tokens = findTokens 0; + in + tokens.open + string + tokens.close; + + /** + Convert the given String into a Vimscript [:let-heredoc]. + For example, you could use this to invoke [:lua]. + + [:let-heredoc]: https://neovim.io/doc/user/eval.html#%3Alet-heredoc + [:lua]: https://neovim.io/doc/user/lua.html#%3Alua-heredoc + + # Examples + + ```nix + toVimscriptHeredoc "simple" + => "<< EOF\nsimple\nEOF" + ``` + + ```nix + toVimscriptHeredoc "EOF" + => "<< EOFF\nEOF\nEOFF" + ``` + + # Type + + ``` + toVimscriptHeredoc :: String -> String + ``` + */ + toVimscriptHeredoc = + string: + let + findToken = + depth: + let + token = "EOF" + lib.strings.replicate depth "F"; + in + if lib.hasInfix token string then findToken (depth + 1) else token; + + token = findToken 0; + in + '' + << ${token} + ${string} + ${token}''; + + # Wrap Vimscript for using in lua, + # but only if the string contains something other than whitespaces + wrapVimscriptForLua = + string: lib.optionalString (hasContent string) "vim.cmd(${toLuaLongLiteral string})"; + + # Wrap lua script for using in Vimscript, + # but only if the string contains something other than whitespaces + wrapLuaForVimscript = + string: lib.optionalString (hasContent string) "lua ${toVimscriptHeredoc string}"; + + # Split a list into a several sub-list, each with a max-size of `size` + groupListBySize = + size: list: + lib.reverseList ( + lib.foldl' ( + lists: item: + let + first = lib.head lists; + rest = lib.drop 1 lists; + in + if lists == [ ] then + [ [ item ] ] + else if lib.length first < size then + [ (first ++ [ item ]) ] ++ rest + else + [ [ item ] ] ++ lists + ) [ ] list + ); +} diff --git a/lib/utils.nix b/lib/utils.nix index 133c3b3efa..d218220b16 100644 --- a/lib/utils.nix +++ b/lib/utils.nix @@ -3,48 +3,67 @@ _nixvimTests, }: rec { - # Whether a string contains something other than whitespaces - hasContent = str: builtins.match "[[:space:]]*" str == null; + /** + Transforms a list to an _"unkeyed"_ attribute set. + + This allows to define mixed table/list in lua: + + ```nix + listToUnkeyedAttrs ["a" "b"] // { foo = "bar"; } + ``` - # Concatenate a list of strings, adding a newline at the end of each one, - # but skipping strings containing only whitespace characters - concatNonEmptyLines = lines: lib.concatLines (builtins.filter hasContent lines); + Resulting in the following lua: + ```lua + {"a", "b", foo = "bar"} + ``` + */ listToUnkeyedAttrs = list: builtins.listToAttrs (lib.lists.imap0 (idx: lib.nameValuePair "__unkeyed-${toString idx}") list); + /** + Usually `true`, except when nixvim is being evaluated by + `mkTestDerivationFromNixvimModule`, where it is `false`. + + This can be used to dynamically enable plugins that can't be run in the + test environment. + */ + # TODO: replace and deprecate + # We shouldn't need to use another instance of `lib` when building a test drv enableExceptInTests = !_nixvimTests; + /** + An empty lua table `{ }` that will be included in the final lua configuration. + + This is equivalent to `{ __empty = { }; }`. + This form can allow to do `option.__empty = { }`. + */ emptyTable = { + # Keep nested to bind docs to `emptyTable`, not `__empty`. "__empty" = null; }; /** - Add a prefix to the keys of an attrs. + Convert the keys of the given attrset to _"raw lua"_. # Example + ```nix - applyPrefixToAttrs "prefix_" { foo = 1; bar = 2; } - => { prefix_foo = 1; prefix_bar = 2; } + toRawKeys { foo = 1; bar = 2; } + => { __rawKey__foo = 1; __rawKey__bar = 2; } ``` - # Type + Resulting in the following lua: + ```lua + {[foo] = 1, [bar] = 2} ``` - applyPrefixToAttrs :: String -> AttrSet -> AttrSet - ``` - */ - applyPrefixToAttrs = prefix: lib.mapAttrs' (n: lib.nameValuePair (prefix + n)); - /** - Turn all the keys of an attrs into raw lua. + If "raw keys" are **not** used, the attr names are treated as string keys: - # Example - - ```nix - toRawKeys { foo = 1; bar = 2; } - => { __rawKey__foo = 1; __rawKey__bar = 2; } + ```lua + {foo = 1, bar = 2} ``` # Type @@ -78,58 +97,12 @@ rec { */ mkRawKey = n: v: toRawKeys { "${n}" = v; }; - /* - Convert a string from camelCase to snake_case - Type: string -> string - */ - toSnakeCase = - let - splitByWords = builtins.split "([A-Z])"; - processWord = s: if lib.isString s then s else "_" + lib.toLower (lib.elemAt s 0); - in - string: - let - words = splitByWords string; - in - lib.concatStrings (map processWord words); - - /** - Those helpers control the lua sections split in `pre, content, post` - */ - mkBeforeSection = lib.mkOrder 300; - mkAfterSection = lib.mkOrder 2000; - /** - Capitalize a string by making the first character uppercase. + Write the string `str` as raw lua in the final lua configuration. - # Example - - ```nix - upperFirstChar "hello, world!" - => "Hello, world!" - ``` - - # Type - - ``` - upperFirstChar :: String -> String - ``` + This is equivalent to `{ __raw = "lua code"; }`. + This form can allow to do `option.__raw = "lua code"`. */ - upperFirstChar = - s: - let - first = lib.substring 0 1 s; - rest = lib.substring 1 (lib.stringLength s) s; - result = (lib.toUpper first) + rest; - in - lib.optionalString (s != "") result; - - mkIfNonNull' = x: y: (lib.mkIf (x != null) y); - - mkIfNonNull = x: (mkIfNonNull' x x); - - ifNonNull' = x: y: if (x == null) then null else y; - mkRaw = r: if r == null || r == "" then @@ -140,273 +113,4 @@ rec { r else throw "mkRaw: invalid input: ${lib.generators.toPretty { multiline = false; } r}"; - - /** - Convert the given string into a literalExpression mkRaw. - - For use in option documentation, such as examples and defaults. - - # Example - - ```nix - literalLua "print('hi')" - => literalExpression ''lib.nixvim.mkRaw "print('hi')"'' - => { - _type = "literalExpression"; - text = ''lib.nixvim.mkRaw "print('hi')"''; - } - ``` - - # Type - - ``` - literalLua :: String -> AttrSet - ``` - */ - literalLua = - r: - let - # Pass the value through mkRaw for validation - raw = mkRaw r; - # TODO: consider switching to lib.generators.mkLuaInline ? - exp = "lib.nixvim.mkRaw " + lib.generators.toPretty { } raw.__raw; - in - lib.literalExpression exp; - - __messagePrefix = scope: "Nixvim (${scope}):"; - /** - Process one or several assertions by prepending the common 'Nixvim (): ' prefix to messages. - The second argument can either be a list of assertions or a single one. - - # Example - - ```nix - assertions = mkAssertions "plugins.foo" { - assertion = plugins.foo.settings.barIntegration && (!plugins.bar.enable); - message = "`barIntegration` is enabled but the `bar` plugin is not." - } - ``` - - # Type - - ``` - mkAssertions :: String -> List -> List - ``` - */ - mkAssertions = - scope: assertions: - let - prefix = __messagePrefix scope; - processAssertion = a: { - inherit (a) assertion; - message = "${prefix} ${lib.trim a.message}"; - }; - in - builtins.map processAssertion (lib.toList assertions); - - /** - Convert one or several conditional warnings to a final warning list. - The second argument can either be a list of _conditional warnings_ or a single one. - - # Example - - ```nix - warnings = mkWarnings "plugins.foo" { - when = plugins.foo.settings.barIntegration && (!plugins.bar.enable); - message = "`barIntegration` is enabled but the `bar` plugin is not." - } - ``` - - # Type - - ``` - mkWarnings :: String -> List -> List - ``` - */ - mkWarnings = - scope: warnings: - let - prefix = __messagePrefix scope; - processWarning = - warning: lib.optional (warning.when or true) "${prefix} ${lib.trim (warning.message or warning)}"; - in - builtins.concatMap processWarning (lib.toList warnings); - - /** - Convert the given string into a `__pretty` printed mkRaw expression. - - For use in nested values that will be printed by `lib.generators.toPretty`, - or `lib.options.renderOptionValue`. - - # Example - - ```nix - nestedLiteralLua "print('hi')" - => nestedLiteral (literalLua "print('hi')") - => { - __pretty = lib.getAttr "text"; - val = literalLua "print('hi')"; - } - ``` - - # Type - - ``` - nestedLiteralLua :: String -> AttrSet - ``` - */ - nestedLiteralLua = r: nestedLiteral (literalLua r); - - /** - Convert the given string or literalExpression into a `__pretty` printed expression. - - For use in nested values that will be printed by `lib.generators.toPretty`, - or `lib.options.renderOptionValue`. - - # Examples - - ```nix - nestedLiteral "example" - => { - __pretty = lib.getAttr "text"; - val = literalExpression "example"; - } - ``` - - ```nix - nestedLiteral (literalExpression ''"hello, world"'') - => { - __pretty = lib.getAttr "text"; - val = literalExpression ''"hello, world"''; - } - ``` - - # Type - - ``` - nestedLiteral :: (String | literalExpression) -> AttrSet - ``` - */ - nestedLiteral = val: { - __pretty = lib.getAttr "text"; - val = if val._type or null == "literalExpression" then val else lib.literalExpression val; - }; - - wrapDo = string: '' - do - ${string} - end - ''; - - /** - Convert the given String to a Lua [long literal]. - For example, you could use this to safely pass a Vimscript string to the - `vim.cmd` function. - - [long literal]: https://www.lua.org/manual/5.4/manual.html#3.1 - - # Examples - - ```nix - nix-repl> toLuaLongLiteral "simple" - "[[simple]]" - ``` - - ```nix - nix-repl> toLuaLongLiteral "]]" - "[=[]]]=]" - ``` - - # Type - - ``` - toLuaLongLiteral :: String -> String - ``` - */ - toLuaLongLiteral = - string: - let - findTokens = - depth: - let - infix = lib.strings.replicate depth "="; - tokens.open = "[${infix}["; - tokens.close = "]${infix}]"; - in - if lib.hasInfix tokens.close string then findTokens (depth + 1) else tokens; - - tokens = findTokens 0; - in - tokens.open + string + tokens.close; - - /** - Convert the given String into a Vimscript [:let-heredoc]. - For example, you could use this to invoke [:lua]. - - [:let-heredoc]: https://neovim.io/doc/user/eval.html#%3Alet-heredoc - [:lua]: https://neovim.io/doc/user/lua.html#%3Alua-heredoc - - # Examples - - ```nix - toVimscriptHeredoc "simple" - => "<< EOF\nsimple\nEOF" - ``` - - ```nix - toVimscriptHeredoc "EOF" - => "<< EOFF\nEOF\nEOFF" - ``` - - # Type - - ``` - toVimscriptHeredoc :: String -> String - ``` - */ - toVimscriptHeredoc = - string: - let - findToken = - depth: - let - token = "EOF" + lib.strings.replicate depth "F"; - in - if lib.hasInfix token string then findToken (depth + 1) else token; - - token = findToken 0; - in - '' - << ${token} - ${string} - ${token}''; - - # Wrap Vimscript for using in lua, - # but only if the string contains something other than whitespaces - wrapVimscriptForLua = - string: lib.optionalString (hasContent string) "vim.cmd(${toLuaLongLiteral string})"; - - # Wrap lua script for using in Vimscript, - # but only if the string contains something other than whitespaces - wrapLuaForVimscript = - string: lib.optionalString (hasContent string) "lua ${toVimscriptHeredoc string}"; - - # Split a list into a several sub-list, each with a max-size of `size` - groupListBySize = - size: list: - lib.reverseList ( - lib.foldl' ( - lists: item: - let - first = lib.head lists; - rest = lib.drop 1 lists; - in - if lists == [ ] then - [ [ item ] ] - else if lib.length first < size then - [ (first ++ [ item ]) ] ++ rest - else - [ [ item ] ] ++ lists - ) [ ] list - ); }