nix-community.nixvim/ci/rust-analyzer/default.nix
Matt Sturgeon 2b2b1e6d8f ci: rename update-scriptsci
This changes how we think about this directory; it does not need to be
exclusively for scripts related to updates, but should be a place for
any scripts intended to be run by CI workflows.

This mindset should make it easier to develop and test the business
logic of workflows, without always needing to test "in production" on
the nixvim repo or a fork.
2025-06-15 21:23:58 +00:00

234 lines
7.1 KiB
Nix

#
# This derivation creates a Nix file that describes the Nix module that needs to be instantiated
#
# The create file is of the form:
#
# {
# "<rust-analyzer.option.name>" = {
# description = "<option description>";
# type = {
# kind = "<name of the type>";
# # Other values depending on the kind, like values for enum or subTypes for oneOf
# };
# };
# }
#
{
lib,
rust-analyzer,
writeText,
pandoc,
runCommand,
}:
let
packageJSON = "${rust-analyzer.src}/editors/code/package.json";
options = (lib.importJSON packageJSON).contributes.configuration;
generatedStart = lib.lists.findFirstIndex (
e: e == { title = "$generated-start"; }
) (throw "missing generated start") options;
generatedEnd = lib.lists.findFirstIndex (
e: e == { title = "$generated-end"; }
) (throw "missing generated end") options;
# Extract only the generated properties, removing vscode specific options
rustAnalyzerProperties = lib.lists.sublist (generatedStart + 1) (
generatedEnd - generatedStart - 1
) options;
mkRustAnalyzerOptionType =
nullable: property_name: property:
let
inner =
{
type ? null,
enum ? null,
minimum ? null,
maximum ? null,
items ? null,
anyOf ? null,
properties ? null,
# Not used in the function, but anyOf values contain it
enumDescriptions ? null,
}@property:
if enum != null then
{
kind = "enum";
values = enum;
}
else if anyOf != null then
let
possibleTypes = lib.filter (sub: !(sub.type == "null" && nullable)) anyOf;
in
{
kind = "oneOf";
subTypes = builtins.map (
t: mkRustAnalyzerOptionType nullable "${property_name}-sub" t
) possibleTypes;
}
else
(
assert lib.assertMsg (type != null) "property is neither anyOf nor enum, it must have a type";
if lib.isList type then
(
if lib.head type == "null" then
assert lib.assertMsg (
lib.length type == 2
) "Lists starting with null are assumed to mean nullOr, so length 2";
let
innerType = property // {
type = lib.elemAt type 1;
};
inner = mkRustAnalyzerOptionType nullable "${property_name}-inner" innerType;
in
assert lib.assertMsg nullable "nullOr types are not yet handled";
inner
else
let
innerTypes = builtins.map (
t: mkRustAnalyzerOptionType nullable "${property_name}-inner" (property // { type = t; })
) type;
in
{
kind = "oneOf";
subTypes = innerTypes;
}
)
else if type == "array" then
{
kind = "list";
item = mkRustAnalyzerOptionType false "${property_name}-item" items;
}
else if type == "number" || type == "integer" then
{
kind = type;
inherit minimum maximum;
}
else if type == "object" && properties != null then
{
kind = "submodule";
options = lib.mapAttrs (
name: value: mkRustAnalyzerOptionType false "${property_name}.${name}" value
) properties;
}
else if
lib.elem type [
"object"
"string"
"boolean"
]
then
{ kind = type; }
else
throw "Unhandled value in ${property_name}: ${lib.generators.toPretty { } property}"
);
in
builtins.addErrorContext "While creating type for ${property_name}:\n${lib.generators.toPretty { } property}" (
inner property
);
mkRustAnalyzerOption =
property_name:
{
# List all possible values so that we are sure no new values are introduced
default,
markdownDescription,
enum ? null,
enumDescriptions ? null,
anyOf ? null,
minimum ? null,
maximum ? null,
items ? null,
# TODO: add this in the documentation ?
uniqueItems ? null,
type ? null,
}:
let
filteredMarkdownDesc =
# If there is a risk that the string contains an heading filter it out
if lib.hasInfix "# " markdownDescription then
builtins.readFile (
runCommand "filtered-documentation" { inherit markdownDescription; } ''
${lib.getExe pandoc} -o $out -t markdown \
--lua-filter=${./heading_filter.lua} <<<"$markdownDescription"
''
)
else
markdownDescription;
enumDesc =
values: descriptions:
let
valueDesc = builtins.map ({ fst, snd }: ''- ${fst}: ${snd}'') (
lib.lists.zipLists values descriptions
);
in
''
${filteredMarkdownDesc}
Values:
${builtins.concatStringsSep "\n" valueDesc}
'';
in
{
type = mkRustAnalyzerOptionType true property_name {
inherit
type
enum
minimum
maximum
items
anyOf
enumDescriptions
;
};
pluginDefault = default;
description =
let
globalDescription = ''
${filteredMarkdownDesc}
'';
in
if
enum == null && (anyOf == null || builtins.all (subProp: !(lib.hasAttr "enum" subProp)) anyOf)
then
globalDescription
else if enum != null then
assert lib.assertMsg (anyOf == null) "enum + anyOf types are not yet handled";
enumDesc enum enumDescriptions
else
let
subEnums = lib.filter (lib.hasAttr "enum") anyOf;
subEnum =
assert lib.assertMsg (
lib.length subEnums == 1
) "anyOf types may currently only contain a single enum";
lib.head subEnums;
in
if subEnum ? enumDescriptions then
enumDesc subEnum.enum subEnum.enumDescriptions
else
globalDescription;
};
rustAnalyzerOptions = builtins.map (
v:
let
props = lib.attrsToList v.properties;
prop =
assert lib.assertMsg (
lib.length props == 1
) "Rust analyzer configuration items are only supported with a single element";
lib.head props;
in
{
"${prop.name}" = mkRustAnalyzerOption prop.name prop.value;
}
) rustAnalyzerProperties;
in
writeText "rust-analyzer-options.nix" (
"# WARNING: DO NOT EDIT\n"
+ "# This file is generated with packages.<system>.rust-analyzer-options, which is run automatically by CI\n"
+ (lib.generators.toPretty { } (lib.mergeAttrsList rustAnalyzerOptions))
)