diff --git a/nixd/lib/Controller/CodeAction.cpp b/nixd/lib/Controller/CodeAction.cpp index 6d6fbb04e..a002fe0bb 100644 --- a/nixd/lib/Controller/CodeAction.cpp +++ b/nixd/lib/Controller/CodeAction.cpp @@ -7,6 +7,7 @@ #include "Convert.h" #include "CodeActions/AttrName.h" +#include "CodeActions/ConvertToInherit.h" #include "CodeActions/ExtractToFile.h" #include "CodeActions/FlattenAttrs.h" #include "CodeActions/JsonToNix.h" @@ -69,6 +70,8 @@ void Controller::onCodeAction(const lspserver::CodeActionParams &Params, nixf::PositionRange NixfRange = toNixfRange(Range); if (const nixf::Node *N = TU->ast()->descend(NixfRange)) { addAttrNameActions(*N, *TU->parentMap(), FileURI, TU->src(), Actions); + addConvertToInheritAction(*N, *TU->parentMap(), FileURI, TU->src(), + Actions); addFlattenAttrsAction(*N, *TU->parentMap(), FileURI, TU->src(), Actions); addPackAttrsAction(*N, *TU->parentMap(), FileURI, TU->src(), Actions); diff --git a/nixd/lib/Controller/CodeActions/ConvertToInherit.cpp b/nixd/lib/Controller/CodeActions/ConvertToInherit.cpp new file mode 100644 index 000000000..80a21c247 --- /dev/null +++ b/nixd/lib/Controller/CodeActions/ConvertToInherit.cpp @@ -0,0 +1,107 @@ +/// \file +/// \brief Implementation of convert to inherit code action. + +#include "ConvertToInherit.h" +#include "Utils.h" + +#include "../Convert.h" + +#include +#include +#include + +namespace nixd { + +void addConvertToInheritAction(const nixf::Node &N, + const nixf::ParentMapAnalysis &PM, + const std::string &FileURI, llvm::StringRef Src, + std::vector &Actions) { + // Find if we're inside a Binding node + const nixf::Node *BindingNode = PM.upTo(N, nixf::Node::NK_Binding); + if (!BindingNode) + return; + + const auto &Bind = static_cast(*BindingNode); + + // Must have a single-segment path + const auto &Names = Bind.path().names(); + if (Names.size() != 1) + return; + + // Path must be static (not interpolated) + const auto &AttrNameNode = Names[0]; + if (!AttrNameNode || !AttrNameNode->isStatic()) + return; + + const std::string &AttrName = AttrNameNode->staticName(); + + // Must have a value + if (!Bind.value()) + return; + + const nixf::Expr &Value = *Bind.value(); + + // Case 1: { x = x; } -> { inherit x; } + if (Value.kind() == nixf::Node::NK_ExprVar) { + const auto &Var = static_cast(Value); + if (Var.id().name() == AttrName) { + std::string NewText = "inherit " + quoteNixAttrKey(AttrName) + ";"; + Actions.emplace_back(createSingleEditAction( + "Convert to `inherit`", lspserver::CodeAction::REFACTOR_REWRITE_KIND, + FileURI, toLSPRange(Src, Bind.range()), std::move(NewText))); + } + return; + } + + // Case 2: { a = source.a; } -> { inherit (source) a; } + // Also handles { a = lib.nested.a; } -> { inherit (lib.nested) a; } + if (Value.kind() == nixf::Node::NK_ExprSelect) { + const auto &Sel = static_cast(Value); + + // Must not have a default value + if (Sel.defaultExpr()) + return; + + // Must have a path + if (!Sel.path()) + return; + + // Selector path must have at least one segment + const auto &SelPath = Sel.path()->names(); + if (SelPath.empty()) + return; + + // The last segment must be static and match the attribute name + const auto &LastSegment = SelPath.back(); + if (!LastSegment || !LastSegment->isStatic()) + return; + + if (LastSegment->staticName() != AttrName) + return; + + // Build the source expression text: + // - Start with the base expression (e.g., "lib" from lib.nested.x) + // - Add any intermediate path segments (e.g., ".nested") + const nixf::Expr &BaseExpr = Sel.expr(); + std::string SourceText = + Src.slice(BaseExpr.lCur().offset(), BaseExpr.rCur().offset()).str(); + + // Add intermediate path segments (all except the last one) + for (size_t I = 0; I + 1 < SelPath.size(); ++I) { + const auto &Segment = SelPath[I]; + if (!Segment || !Segment->isStatic()) + return; // Bail out if any intermediate segment is dynamic + SourceText += "."; + SourceText += quoteNixAttrKey(Segment->staticName()); + } + + std::string NewText = + "inherit (" + SourceText + ") " + quoteNixAttrKey(AttrName) + ";"; + Actions.emplace_back(createSingleEditAction( + "Convert to `inherit (" + SourceText + ")`", + lspserver::CodeAction::REFACTOR_REWRITE_KIND, FileURI, + toLSPRange(Src, Bind.range()), std::move(NewText))); + } +} + +} // namespace nixd diff --git a/nixd/lib/Controller/CodeActions/ConvertToInherit.h b/nixd/lib/Controller/CodeActions/ConvertToInherit.h new file mode 100644 index 000000000..90f93c9b9 --- /dev/null +++ b/nixd/lib/Controller/CodeActions/ConvertToInherit.h @@ -0,0 +1,42 @@ +/// \file +/// \brief Code action for converting bindings to inherit syntax. +/// +/// Transforms: +/// - { x = x; } -> { inherit x; } +/// - { a = lib.a; } -> { inherit (lib) a; } + +#pragma once + +#include + +#include + +#include + +#include +#include + +namespace nixf { +class Node; +} // namespace nixf + +namespace nixd { + +/// \brief Add code action to convert binding to inherit syntax. +/// +/// This action is offered when the cursor is on a binding that matches +/// one of these patterns: +/// - { attrName = attrName; } -> { inherit attrName; } +/// - { attrName = source.attrName; } -> { inherit (source) attrName; } +/// +/// The action is NOT offered when: +/// - The binding has a multi-segment path (e.g., x.y = z) +/// - The attribute name is dynamic/interpolated +/// - The value doesn't match the attribute name +/// - The select expression has a default value +void addConvertToInheritAction(const nixf::Node &N, + const nixf::ParentMapAnalysis &PM, + const std::string &FileURI, llvm::StringRef Src, + std::vector &Actions); + +} // namespace nixd diff --git a/nixd/lib/meson.build b/nixd/lib/meson.build index a37168ee2..ca5711a1d 100644 --- a/nixd/lib/meson.build +++ b/nixd/lib/meson.build @@ -9,6 +9,7 @@ libnixd_lib = library( 'Controller/AST.cpp', 'Controller/CodeAction.cpp', 'Controller/CodeActions/AttrName.cpp', + 'Controller/CodeActions/ConvertToInherit.cpp', 'Controller/CodeActions/ExtractToFile.cpp', 'Controller/CodeActions/FlattenAttrs.cpp', 'Controller/CodeActions/JsonToNix.cpp', diff --git a/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-from-nested.md b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-from-nested.md new file mode 100644 index 000000000..b2781155c --- /dev/null +++ b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-from-nested.md @@ -0,0 +1,71 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///convert-to-inherit-from-nested.nix +{ x = lib.nested.x; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///convert-to-inherit-from-nested.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":2 + }, + "end":{ + "line":0, + "character":19 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ +``` + +``` +CHECK: "newText": "inherit (lib.nested) x;" +CHECK: "kind": "refactor.rewrite" +CHECK: "title": "Convert to `inherit (lib.nested)`" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-from-simple.md b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-from-simple.md new file mode 100644 index 000000000..64ebcd4c3 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-from-simple.md @@ -0,0 +1,71 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///convert-to-inherit-from-simple.nix +{ foo = lib.foo; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///convert-to-inherit-from-simple.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":2 + }, + "end":{ + "line":0, + "character":16 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ +``` + +``` +CHECK: "newText": "inherit (lib) foo;" +CHECK: "kind": "refactor.rewrite" +CHECK: "title": "Convert to `inherit (lib)`" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-mismatch.md b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-mismatch.md new file mode 100644 index 000000000..a79d839da --- /dev/null +++ b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-mismatch.md @@ -0,0 +1,68 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +# Test that no convert-to-inherit action is offered when variable name +# doesn't match attribute name + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///convert-to-inherit-mismatch.nix +{ foo = bar; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///convert-to-inherit-mismatch.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":2 + }, + "end":{ + "line":0, + "character":12 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [] +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-multi-segment.md b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-multi-segment.md new file mode 100644 index 000000000..658ae9ffc --- /dev/null +++ b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-multi-segment.md @@ -0,0 +1,68 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +# Test that no convert-to-inherit action is offered for multi-segment paths + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///convert-to-inherit-multi-segment.nix +{ foo.bar = bar; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///convert-to-inherit-multi-segment.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":2 + }, + "end":{ + "line":0, + "character":16 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ +CHECK-NOT: "Convert to `inherit`" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-quoted.md b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-quoted.md new file mode 100644 index 000000000..3b1f11230 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-quoted.md @@ -0,0 +1,75 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +# Test convert-to-inherit with quoted attribute name containing hyphen +# Note: foo-bar is a valid Nix identifier (hyphens allowed), so no quotes needed +# { "foo-bar" = foo-bar; } -> { inherit foo-bar; } + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///convert-to-inherit-quoted.nix +{ "foo-bar" = foo-bar; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///convert-to-inherit-quoted.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":2 + }, + "end":{ + "line":0, + "character":22 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ +``` + +``` +CHECK: "newText": "inherit foo-bar;" +CHECK: "kind": "refactor.rewrite" +CHECK: "title": "Convert to `inherit`" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-rec.md b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-rec.md new file mode 100644 index 000000000..1ec3e955e --- /dev/null +++ b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-rec.md @@ -0,0 +1,75 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +# Test that convert-to-inherit action IS offered in rec attribute sets +# Note: Converting { foo = foo; } to { inherit foo; } is still valid in rec {} +# because inherit takes from lexical scope, not from the rec set itself. + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///convert-to-inherit-rec.nix +rec { foo = foo; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///convert-to-inherit-rec.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":6 + }, + "end":{ + "line":0, + "character":16 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ +``` + +``` +CHECK: "newText": "inherit foo;" +CHECK: "kind": "refactor.rewrite" +CHECK: "title": "Convert to `inherit`" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-selector-mismatch.md b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-selector-mismatch.md new file mode 100644 index 000000000..b13ac1ba4 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-selector-mismatch.md @@ -0,0 +1,68 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +# Test that no convert-to-inherit action is offered when selector name +# doesn't match attribute name + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///convert-to-inherit-selector-mismatch.nix +{ foo = lib.bar; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///convert-to-inherit-selector-mismatch.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":2 + }, + "end":{ + "line":0, + "character":16 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [] +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-simple.md b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-simple.md new file mode 100644 index 000000000..457b878f2 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-simple.md @@ -0,0 +1,73 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +# Test simple convert-to-inherit: { foo = foo; } -> { inherit foo; } + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///convert-to-inherit-simple.nix +{ foo = foo; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///convert-to-inherit-simple.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":2 + }, + "end":{ + "line":0, + "character":12 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ +``` + +``` +CHECK: "newText": "inherit foo;" +CHECK: "kind": "refactor.rewrite" +CHECK: "title": "Convert to `inherit`" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-with-default.md b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-with-default.md new file mode 100644 index 000000000..ab12bb672 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/convert-to-inherit/convert-to-inherit-with-default.md @@ -0,0 +1,67 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +# Test that no convert-to-inherit action is offered when select has default + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///convert-to-inherit-with-default.nix +{ foo = lib.foo or null; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///convert-to-inherit-with-default.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":2 + }, + "end":{ + "line":0, + "character":24 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [] +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +```