feat(code-action): add pack dotted path to nested set refactor action#758
Conversation
Add a new refactoring code action that transforms dotted attribute paths
into nested attribute sets (inverse of flatten action).
Transformation: { foo.bar = 1; } -> { foo = { bar = 1; }; }
The action is offered when:
- Path has at least 2 segments (e.g., foo.bar)
- All path segments are static identifiers
- Parent attribute set is not recursive (rec)
- No sibling bindings share the same first segment
Explicitly excluded:
- Single-segment paths (nothing to pack)
- Dynamic paths (cannot statically transform)
- Recursive sets (packing may change semantics)
- Sibling conflicts (would create duplicate nested keys)
Test files:
- pack-attrs.md: basic functionality
- pack-attrs-deep.md: deep paths (one-level pack)
- pack-attrs-quoted.md: quoted attribute preservation
- pack-attrs-empty.md: empty attrset value
- pack-attrs-single.md: negative test for single segments
- pack-attrs-rec.md: negative test for recursive sets
- pack-attrs-dynamic.md: negative test for dynamic paths
- pack-attrs-siblings.md: negative test for sibling conflicts
- flatten-attrs-deep.md: verify both actions offered together
inclyc
left a comment
There was a problem hiding this comment.
Is this possible? #755 (comment)
nixd has SemaAttrs where attributes are lowered into nested structure.
|
It seems implementable, so I'll give it a try. |
- Add logic to detect and offer bulk pack action when multiple sibling bindings share the same prefix - Quote and escape attribute keys as needed for valid Nix syntax - Prevent pack action when siblings have dynamic attributes or path conflicts - Refactor code to use getSiblingCount and findSiblingBindingsRange for robust sibling analysis - Add tests for quoted keys, escaping, multiline sets, empty sets, complex values, and positive/negative sibling cases
… keys - Move attribute key quoting and escaping logic into quoteNixAttrKey - Replace duplicated key quoting code with quoteNixAttrKey calls - Improve maintainability and consistency for Nix attribute key handling
- Add logic to process escape sequences (\n, \r, \t, etc.) in strings - Unescape recognized sequences and add unescaped characters to Parts - Update comments for clarity on escape handling
- Add test for parsing string escape sequences: \n, \r, \t, \\, \", \$ - Add test for parsing strings with escaped quotes - Ensure diagnostics are empty and literals match expected values
Split pack-attrs-siblings-positive.md into two separate test files to fix the CI failure in the nix-build-develop-gcc (undefined, release, false) configuration. The original test contained 2 sequential code action requests followed by an exit notification. Under UBSan, the sanitizer overhead caused a race condition where the second response wasn't fully flushed to stdout before process termination. Following the pattern from PR #757, each test file now contains only ONE code action request, eliminating the race condition. New test files: - pack-attrs-sibling-inherit.md: Tests pack action with inherit sibling - pack-attrs-sibling-different-prefix.md: Tests pack with different prefix
inclyc
left a comment
There was a problem hiding this comment.
The current implementation LGTM. However, I think we can further improve the UX.
I suggest we handle this as a follow-up task in a separate PR to keep this one focused.
| Actions.emplace_back(createSingleEditAction( | ||
| "Pack dotted path to nested set", CodeAction::REFACTOR_REWRITE_KIND, | ||
| FileURI, toLSPRange(Src, Bind.range()), std::move(NewText))); | ||
| } else { |
There was a problem hiding this comment.
Offer both 'Pack One' and 'Pack All' options?
e.g. sometimes user may just want to "pack one"
{
a.b = 1;
a.c = 2;
}->
{
a = { b = 1; };
a.c = 2;
}| return; | ||
|
|
||
| Actions.emplace_back(createSingleEditAction( | ||
| "Pack all '" + FirstSeg + "' bindings to nested set", |
There was a problem hiding this comment.
| "Pack all '" + FirstSeg + "' bindings to nested set", | |
| "Recursively pack all '" + FirstSeg + "' bindings to nested set", |
Right now, the 'Pack All' operation actually behaves like a recursive pack (matching SemaAttrs logic).
i.e.
{
a.a = 1;
a.f = {
a.x.x2.x3.x4 = 1;
};
}currently it will be packed into this:
{
a = {
a = 1;
f = {
a = {
x = {
x2 = {
x3 = {
x4 = 1;
};
};
};
};
};
};
}Sometimes, users might prefer a 'shallow pack' that only expands the first level of attributes.
|
This PR ended up with too many changes, so I'll split it into a separate PR!! Thanks for the review!! I learned a lot about C++ again this time |

Summary
Add a new code action that converts dotted attribute paths to nested attribute sets - the inverse of the "Flatten nested attribute set" action from #757.
Single Pack:
Bulk Pack (multiple sibling bindings with same prefix):
Changes
{ foo = 1; foo.bar = 2; })Behavior
Single Pack Action ("Pack dotted path to nested set")
Action is offered when:
foo.bar)Bulk Pack Action ("Pack all 'X' bindings to nested set")
Action is offered when:
foo.barandfoo.baz)Transformation combines all matching siblings:
Explicitly Excluded
foo = 1): Nothing to packrec { ... }): Packing would change semanticsfoo.${bar}): Cannot safely transform interpolated paths{ foo = 1; foo.bar = 2; }): Would create invalid Nix codeDeep Path Handling
Deep paths are packed one level at a time:
a.b.c = 1→a = { b.c = 1; }(first application)a = { b.c = 1; }→a = { b = { c = 1; }; }(second application)Key Quoting and Escaping
The action correctly handles:
"1foo") are properly quoted in output",\, and$are correctly escapedTest Plan
Related