From 95076e58aefc425809b6d28f0d2bd9b3635360c9 Mon Sep 17 00:00:00 2001 From: takeokunn Date: Tue, 13 Jan 2026 22:39:32 +0900 Subject: [PATCH] feat(code-action): add "inherit to binding" code action for single-name inherits - Introduce addInheritToBindingAction to convert single-name inherit statements to explicit bindings - Support both simple and expression-based inherits, e.g.: - { inherit x; } -> { x = x; } - { inherit (b) a; } -> { a = b.a; } - Only offer the action when the inherit statement has exactly one name - Add tests for basic, complex, long-name, and negative multi-name cases - Register new code action in build system and controller --- nixd/lib/Controller/CodeAction.cpp | 3 + .../CodeActions/InheritToBinding.cpp | 66 ++++++++++++++++++ .../Controller/CodeActions/InheritToBinding.h | 37 ++++++++++ nixd/lib/meson.build | 1 + .../code-action/inherit-to-binding/basic.md | 68 ++++++++++++++++++ .../inherit-to-binding/complex-expr.md | 68 ++++++++++++++++++ .../inherit-to-binding/long-name.md | 68 ++++++++++++++++++ .../inherit-to-binding/multi-name-negative.md | 69 +++++++++++++++++++ .../inherit-to-binding/with-expr.md | 68 ++++++++++++++++++ 9 files changed, 448 insertions(+) create mode 100644 nixd/lib/Controller/CodeActions/InheritToBinding.cpp create mode 100644 nixd/lib/Controller/CodeActions/InheritToBinding.h create mode 100644 nixd/tools/nixd/test/code-action/inherit-to-binding/basic.md create mode 100644 nixd/tools/nixd/test/code-action/inherit-to-binding/complex-expr.md create mode 100644 nixd/tools/nixd/test/code-action/inherit-to-binding/long-name.md create mode 100644 nixd/tools/nixd/test/code-action/inherit-to-binding/multi-name-negative.md create mode 100644 nixd/tools/nixd/test/code-action/inherit-to-binding/with-expr.md diff --git a/nixd/lib/Controller/CodeAction.cpp b/nixd/lib/Controller/CodeAction.cpp index 6d6fbb04e..d9096ef33 100644 --- a/nixd/lib/Controller/CodeAction.cpp +++ b/nixd/lib/Controller/CodeAction.cpp @@ -9,6 +9,7 @@ #include "CodeActions/AttrName.h" #include "CodeActions/ExtractToFile.h" #include "CodeActions/FlattenAttrs.h" +#include "CodeActions/InheritToBinding.h" #include "CodeActions/JsonToNix.h" #include "CodeActions/NoogleDoc.h" #include "CodeActions/PackAttrs.h" @@ -72,6 +73,8 @@ void Controller::onCodeAction(const lspserver::CodeActionParams &Params, addFlattenAttrsAction(*N, *TU->parentMap(), FileURI, TU->src(), Actions); addPackAttrsAction(*N, *TU->parentMap(), FileURI, TU->src(), Actions); + addInheritToBindingAction(*N, *TU->parentMap(), FileURI, TU->src(), + Actions); addNoogleDocAction(*N, *TU->parentMap(), Actions); // Extract to file requires variable lookup analysis diff --git a/nixd/lib/Controller/CodeActions/InheritToBinding.cpp b/nixd/lib/Controller/CodeActions/InheritToBinding.cpp new file mode 100644 index 000000000..faa5fbee9 --- /dev/null +++ b/nixd/lib/Controller/CodeActions/InheritToBinding.cpp @@ -0,0 +1,66 @@ +/// \file +/// \brief Implementation of inherit to explicit binding code action. + +#include "InheritToBinding.h" +#include "Utils.h" + +#include "../Convert.h" + +#include + +#include + +namespace nixd { + +void addInheritToBindingAction(const nixf::Node &N, + const nixf::ParentMapAnalysis &PM, + const std::string &FileURI, llvm::StringRef Src, + std::vector &Actions) { + // Find if we're inside an Inherit node + const nixf::Node *InheritNode = PM.upTo(N, nixf::Node::NK_Inherit); + if (!InheritNode) + return; + + const auto &Inherit = static_cast(*InheritNode); + + // Get the list of names in the inherit statement + const auto &Names = Inherit.names(); + + // Only handle single-name inherit statements + // Multi-name inherits would require more complex handling + if (Names.size() != 1) + return; + + const auto &Name = Names[0]; + + // Defensive null check + if (!Name) + return; + + // Only handle static names (not interpolated) + if (!Name->isStatic()) + return; + + const std::string &AttrName = Name->staticName(); + + // Build the replacement text + std::ostringstream NewText; + NewText << AttrName << " = "; + + if (Inherit.expr()) { + // inherit (expr) name; -> name = expr.name; + NewText << Inherit.expr()->src(Src) << "." << AttrName; + } else { + // inherit name; -> name = name; + NewText << AttrName; + } + NewText << ";"; + + // Create the code action + Actions.emplace_back(createSingleEditAction( + "Convert to explicit binding", + lspserver::CodeAction::REFACTOR_REWRITE_KIND, FileURI, + toLSPRange(Src, Inherit.range()), NewText.str())); +} + +} // namespace nixd diff --git a/nixd/lib/Controller/CodeActions/InheritToBinding.h b/nixd/lib/Controller/CodeActions/InheritToBinding.h new file mode 100644 index 000000000..ede708b61 --- /dev/null +++ b/nixd/lib/Controller/CodeActions/InheritToBinding.h @@ -0,0 +1,37 @@ +/// \file +/// \brief Code action for converting inherit to explicit binding. +/// +/// Offers to convert inherit statements to explicit bindings: +/// - { inherit x; } -> { x = x; } +/// - { inherit (b) a; } -> { a = b.a; } + +#pragma once + +#include + +#include + +#include + +#include +#include + +namespace nixf { +class Node; +} // namespace nixf + +namespace nixd { + +/// \brief Add code action to convert inherit to explicit binding. +/// +/// This action converts inherit statements to explicit bindings: +/// - Simple inherit: { inherit x; } -> { x = x; } +/// - Inherit from expression: { inherit (b) a; } -> { a = b.a; } +/// +/// The action is only offered when the inherit has exactly one name. +void addInheritToBindingAction(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..7df1b50b7 100644 --- a/nixd/lib/meson.build +++ b/nixd/lib/meson.build @@ -11,6 +11,7 @@ libnixd_lib = library( 'Controller/CodeActions/AttrName.cpp', 'Controller/CodeActions/ExtractToFile.cpp', 'Controller/CodeActions/FlattenAttrs.cpp', + 'Controller/CodeActions/InheritToBinding.cpp', 'Controller/CodeActions/JsonToNix.cpp', 'Controller/CodeActions/NoogleDoc.cpp', 'Controller/CodeActions/PackAttrs.cpp', diff --git a/nixd/tools/nixd/test/code-action/inherit-to-binding/basic.md b/nixd/tools/nixd/test/code-action/inherit-to-binding/basic.md new file mode 100644 index 000000000..637901cb1 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/inherit-to-binding/basic.md @@ -0,0 +1,68 @@ +# 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:///basic-inherit.nix +{ inherit x; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///basic-inherit.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":10 + }, + "end":{ + "line":0, + "character":11 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ + CHECK: "newText": "x = x;", + CHECK: "kind": "refactor.rewrite", +CHECK-NEXT: "title": "Convert to explicit binding" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/inherit-to-binding/complex-expr.md b/nixd/tools/nixd/test/code-action/inherit-to-binding/complex-expr.md new file mode 100644 index 000000000..5ad3cae29 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/inherit-to-binding/complex-expr.md @@ -0,0 +1,68 @@ +# 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:///inherit-complex-expr.nix +{ inherit (pkgs.lib) mkOption; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///inherit-complex-expr.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":21 + }, + "end":{ + "line":0, + "character":29 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ + CHECK: "newText": "mkOption = pkgs.lib.mkOption;", + CHECK: "kind": "refactor.rewrite", +CHECK-NEXT: "title": "Convert to explicit binding" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/inherit-to-binding/long-name.md b/nixd/tools/nixd/test/code-action/inherit-to-binding/long-name.md new file mode 100644 index 000000000..da6c9b33d --- /dev/null +++ b/nixd/tools/nixd/test/code-action/inherit-to-binding/long-name.md @@ -0,0 +1,68 @@ +# 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:///inherit-long-name.nix +{ inherit fooBar; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///inherit-long-name.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":10 + }, + "end":{ + "line":0, + "character":16 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ + CHECK: "newText": "fooBar = fooBar;", + CHECK: "kind": "refactor.rewrite", +CHECK-NEXT: "title": "Convert to explicit binding" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/inherit-to-binding/multi-name-negative.md b/nixd/tools/nixd/test/code-action/inherit-to-binding/multi-name-negative.md new file mode 100644 index 000000000..20bf0a41d --- /dev/null +++ b/nixd/tools/nixd/test/code-action/inherit-to-binding/multi-name-negative.md @@ -0,0 +1,69 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +## Test: Multiple names in inherit should NOT offer the inherit-to-binding action +## (but may offer other actions like Quote attribute name) + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///multi-inherit.nix +{ inherit x y; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///multi-inherit.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":10 + }, + "end":{ + "line":0, + "character":11 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ + CHECK-NOT: "title": "Convert to explicit binding" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/inherit-to-binding/with-expr.md b/nixd/tools/nixd/test/code-action/inherit-to-binding/with-expr.md new file mode 100644 index 000000000..302267810 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/inherit-to-binding/with-expr.md @@ -0,0 +1,68 @@ +# 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:///inherit-with-expr.nix +{ inherit (b) a; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///inherit-with-expr.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":14 + }, + "end":{ + "line":0, + "character":15 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ + CHECK: "newText": "a = b.a;", + CHECK: "kind": "refactor.rewrite", +CHECK-NEXT: "title": "Convert to explicit binding" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +```