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 @@ -14,6 +14,7 @@
#include "CodeActions/JsonToNix.h"
#include "CodeActions/NoogleDoc.h"
#include "CodeActions/PackAttrs.h"
#include "CodeActions/RewriteString.h"
#include "CodeActions/WithToLet.h"

#include "nixd/Controller/Controller.h"
Expand Down Expand Up @@ -80,6 +81,8 @@ void Controller::onCodeAction(const lspserver::CodeActionParams &Params,
addInheritToBindingAction(*N, *TU->parentMap(), FileURI, TU->src(),
Actions);
addNoogleDocAction(*N, *TU->parentMap(), Actions);
addRewriteStringAction(*N, *TU->parentMap(), FileURI, TU->src(),
Actions);

// Extract to file requires variable lookup analysis
if (TU->variableLookup()) {
Expand Down
144 changes: 144 additions & 0 deletions nixd/lib/Controller/CodeActions/RewriteString.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/// \file
/// \brief Implementation of string literal syntax conversion code action.

#include "RewriteString.h"
#include "Utils.h"

#include "../Convert.h"

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

namespace nixd {

namespace {

/// \brief Check if the string at the given source position is indented style.
///
/// Indented strings start with '' while double-quoted strings start with ".
bool isIndentedString(llvm::StringRef Src,
const nixf::LexerCursorRange &Range) {
size_t Offset = Range.lCur().offset();
if (Offset + 1 >= Src.size())
return false;
return Src[Offset] == '\'' && Src[Offset + 1] == '\'';
}

/// \brief Escape a string for indented string literal syntax.
///
/// Escapes:
/// - '' -> ''' (two single quotes become three)
/// - ${ -> ''${ (prevent interpolation)
std::string escapeForIndentedString(llvm::StringRef S) {
std::string Result;
Result.reserve(S.size() + S.size() / 8);

for (size_t I = 0; I < S.size(); ++I) {
char C = S[I];

// Check for '' sequence that needs escaping
if (C == '\'' && I + 1 < S.size() && S[I + 1] == '\'') {
Result += "'''";
++I; // Skip the second quote
continue;
}

// Check for ${ that needs escaping
if (C == '$' && I + 1 < S.size() && S[I + 1] == '{') {
Result += "''${";
++I; // Skip the '{'
continue;
}

Result += C;
}

return Result;
}

/// \brief Build a double-quoted string from InterpolatedParts.
///
/// Iterates through fragments, escaping text parts and preserving
/// interpolations from source.
std::string buildDoubleQuotedString(const nixf::InterpolatedParts &Parts,
llvm::StringRef Src) {
std::string Result = "\"";

for (const auto &Frag : Parts.fragments()) {
if (Frag.kind() == nixf::InterpolablePart::SPK_Escaped) {
// Escape the unescaped content for double-quoted string
Result += escapeNixString(Frag.escaped());
} else {
// SPK_Interpolation: preserve source text exactly
const nixf::Interpolation &Interp = Frag.interpolation();
Result += Interp.src(Src);
}
}

Result += "\"";
return Result;
}

/// \brief Build an indented string from InterpolatedParts.
///
/// Iterates through fragments, escaping text parts and preserving
/// interpolations from source.
std::string buildIndentedString(const nixf::InterpolatedParts &Parts,
llvm::StringRef Src) {
std::string Result = "''";

for (const auto &Frag : Parts.fragments()) {
if (Frag.kind() == nixf::InterpolablePart::SPK_Escaped) {
// Escape the unescaped content for indented string
Result += escapeForIndentedString(Frag.escaped());
} else {
// SPK_Interpolation: preserve source text exactly
const nixf::Interpolation &Interp = Frag.interpolation();
Result += Interp.src(Src);
}
}

Result += "''";
return Result;
}

} // namespace

void addRewriteStringAction(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 an ExprString
const nixf::Node *StringNode = PM.upTo(N, nixf::Node::NK_ExprString);
if (!StringNode)
return;

// Skip if this string is used as an attribute name (handled by AttrName
// actions)
if (PM.upTo(*StringNode, nixf::Node::NK_AttrName))
return;

const auto &Str = static_cast<const nixf::ExprString &>(*StringNode);

// Determine current string type
bool IsIndented = isIndentedString(Src, Str.range());

// Build the converted string
std::string NewText;
std::string Title;

if (IsIndented) {
// Convert indented -> double-quoted
NewText = buildDoubleQuotedString(Str.parts(), Src);
Title = "Convert to double-quoted string";
} else {
// Convert double-quoted -> indented
NewText = buildIndentedString(Str.parts(), Src);
Title = "Convert to indented string";
}

Actions.emplace_back(createSingleEditAction(
Title, lspserver::CodeAction::REFACTOR_REWRITE_KIND, FileURI,
toLSPRange(Src, Str.range()), std::move(NewText)));
}

} // namespace nixd
33 changes: 33 additions & 0 deletions nixd/lib/Controller/CodeActions/RewriteString.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/// \file
/// \brief Code action for converting between string literal syntaxes.
///
/// Transforms between double-quoted ("...") and indented (''...'') strings.

#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 rewrite action for string literal syntax conversion.
///
/// This action is offered when the cursor is on a string literal.
/// It converts between double-quoted strings ("...") and indented
/// strings (''...''), properly handling escape sequence differences.
void addRewriteStringAction(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 @@ -16,6 +16,7 @@ libnixd_lib = library(
'Controller/CodeActions/JsonToNix.cpp',
'Controller/CodeActions/NoogleDoc.cpp',
'Controller/CodeActions/PackAttrs.cpp',
'Controller/CodeActions/RewriteString.cpp',
'Controller/CodeActions/Utils.cpp',
'Controller/CodeActions/WithToLet.cpp',
'Controller/Completion.cpp',
Expand Down
68 changes: 68 additions & 0 deletions nixd/tools/nixd/test/code-action/rewrite-string/empty-string.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# RUN: nixd --lit-test < %s | FileCheck %s

Test that empty strings can be converted between formats.

<-- initialize(0)

```json
{
"jsonrpc":"2.0",
"id":0,
"method":"initialize",
"params":{
"processId":123,
"rootPath":"",
"capabilities":{
},
"trace":"off"
}
}
```


<-- textDocument/didOpen

```nix file:///empty-string.nix
{ x = ""; }
```

<-- textDocument/codeAction(2)


```json
{
"jsonrpc":"2.0",
"id":2,
"method":"textDocument/codeAction",
"params":{
"textDocument":{
"uri":"file:///empty-string.nix"
},
"range":{
"start":{
"line": 0,
"character":6
},
"end":{
"line":0,
"character":8
}
},
"context":{
"diagnostics":[],
"triggerKind":2
}
}
}
```

```
CHECK: "id": 2,
CHECK: "result": [
CHECK: "newText": "''''"
CHECK: "title": "Convert to indented string"
```

```json
{"jsonrpc":"2.0","method":"exit"}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# RUN: nixd --lit-test < %s | FileCheck %s

Test that backslash escapes are properly handled when converting to indented string.

<-- initialize(0)

```json
{
"jsonrpc":"2.0",
"id":0,
"method":"initialize",
"params":{
"processId":123,
"rootPath":"",
"capabilities":{
},
"trace":"off"
}
}
```


<-- textDocument/didOpen

```nix file:///escape-backslash.nix
{ x = "path\\to\\file"; }
```

<-- textDocument/codeAction(2)


```json
{
"jsonrpc":"2.0",
"id":2,
"method":"textDocument/codeAction",
"params":{
"textDocument":{
"uri":"file:///escape-backslash.nix"
},
"range":{
"start":{
"line": 0,
"character":6
},
"end":{
"line":0,
"character":22
}
},
"context":{
"diagnostics":[],
"triggerKind":2
}
}
}
```

The backslashes are unescaped to literal backslashes in the indented string.

```
CHECK: "id": 2,
CHECK: "result": [
CHECK: "newText": "''path\\to\\file''"
CHECK: "title": "Convert to indented string"
```

```json
{"jsonrpc":"2.0","method":"exit"}
```
Loading