Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions nixd/lib/Controller/CodeAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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);
Expand Down
107 changes: 107 additions & 0 deletions nixd/lib/Controller/CodeActions/ConvertToInherit.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/// \file
/// \brief Implementation of convert to inherit code action.

#include "ConvertToInherit.h"
#include "Utils.h"

#include "../Convert.h"

#include <nixf/Basic/Nodes/Attrs.h>
#include <nixf/Basic/Nodes/Expr.h>
#include <nixf/Basic/Nodes/Simple.h>

namespace nixd {

void addConvertToInheritAction(const nixf::Node &N,
const nixf::ParentMapAnalysis &PM,
const std::string &FileURI, llvm::StringRef Src,
std::vector<lspserver::CodeAction> &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<const nixf::Binding &>(*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<const nixf::ExprVar &>(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<const nixf::ExprSelect &>(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
42 changes: 42 additions & 0 deletions nixd/lib/Controller/CodeActions/ConvertToInherit.h
Original file line number Diff line number Diff line change
@@ -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 <lspserver/Protocol.h>

#include <nixf/Sema/ParentMap.h>

#include <llvm/ADT/StringRef.h>

#include <string>
#include <vector>

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<lspserver::CodeAction> &Actions);

} // namespace nixd
1 change: 1 addition & 0 deletions nixd/lib/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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"}
```
Original file line number Diff line number Diff line change
@@ -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"}
```
Original file line number Diff line number Diff line change
@@ -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"}
```
Loading