From 3e6a4597bfdafef23a0d7db85893579d39942896 Mon Sep 17 00:00:00 2001 From: takeokunn Date: Wed, 14 Jan 2026 09:37:01 +0900 Subject: [PATCH 1/2] feat(code-action): add rewrite string action for literal syntax conversion - Introduce code action to convert between double-quoted and indented string literals in Nix code - Handle escaping rules and interpolation preservation for both formats - Offer action only when cursor is on a string literal node - Add comprehensive tests for empty strings, escapes, quotes, tabs, newlines, and interpolations - Update build configuration to include new implementation files --- nixd/lib/Controller/CodeAction.cpp | 3 + .../Controller/CodeActions/RewriteString.cpp | 138 ++++++++++++++++++ .../Controller/CodeActions/RewriteString.h | 33 +++++ nixd/lib/meson.build | 1 + .../rewrite-string/empty-string.md | 68 +++++++++ .../rewrite-string/escape-backslash.md | 70 +++++++++ .../rewrite-string/escape-newline.md | 70 +++++++++ .../rewrite-string/escape-quotes.md | 72 +++++++++ .../code-action/rewrite-string/escape-tab.md | 70 +++++++++ .../rewrite-string/interpolation.md | 68 +++++++++ .../code-action/rewrite-string/not-string.md | 71 +++++++++ .../rewrite-string/to-dquote-interpolation.md | 68 +++++++++ .../rewrite-string/to-dquote-newline.md | 73 +++++++++ .../code-action/rewrite-string/to-dquote.md | 68 +++++++++ .../code-action/rewrite-string/to-indented.md | 68 +++++++++ 15 files changed, 941 insertions(+) create mode 100644 nixd/lib/Controller/CodeActions/RewriteString.cpp create mode 100644 nixd/lib/Controller/CodeActions/RewriteString.h create mode 100644 nixd/tools/nixd/test/code-action/rewrite-string/empty-string.md create mode 100644 nixd/tools/nixd/test/code-action/rewrite-string/escape-backslash.md create mode 100644 nixd/tools/nixd/test/code-action/rewrite-string/escape-newline.md create mode 100644 nixd/tools/nixd/test/code-action/rewrite-string/escape-quotes.md create mode 100644 nixd/tools/nixd/test/code-action/rewrite-string/escape-tab.md create mode 100644 nixd/tools/nixd/test/code-action/rewrite-string/interpolation.md create mode 100644 nixd/tools/nixd/test/code-action/rewrite-string/not-string.md create mode 100644 nixd/tools/nixd/test/code-action/rewrite-string/to-dquote-interpolation.md create mode 100644 nixd/tools/nixd/test/code-action/rewrite-string/to-dquote-newline.md create mode 100644 nixd/tools/nixd/test/code-action/rewrite-string/to-dquote.md create mode 100644 nixd/tools/nixd/test/code-action/rewrite-string/to-indented.md diff --git a/nixd/lib/Controller/CodeAction.cpp b/nixd/lib/Controller/CodeAction.cpp index 6d6fbb04e..fd2dbc735 100644 --- a/nixd/lib/Controller/CodeAction.cpp +++ b/nixd/lib/Controller/CodeAction.cpp @@ -12,6 +12,7 @@ #include "CodeActions/JsonToNix.h" #include "CodeActions/NoogleDoc.h" #include "CodeActions/PackAttrs.h" +#include "CodeActions/RewriteString.h" #include "nixd/Controller/Controller.h" @@ -73,6 +74,8 @@ void Controller::onCodeAction(const lspserver::CodeActionParams &Params, Actions); addPackAttrsAction(*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()) { diff --git a/nixd/lib/Controller/CodeActions/RewriteString.cpp b/nixd/lib/Controller/CodeActions/RewriteString.cpp new file mode 100644 index 000000000..23de577d3 --- /dev/null +++ b/nixd/lib/Controller/CodeActions/RewriteString.cpp @@ -0,0 +1,138 @@ +/// \file +/// \brief Implementation of string literal syntax conversion code action. + +#include "RewriteString.h" +#include "Utils.h" + +#include "../Convert.h" + +#include + +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 &Actions) { + // Find if we're inside an ExprString + const nixf::Node *StringNode = PM.upTo(N, nixf::Node::NK_ExprString); + if (!StringNode) + return; + + const auto &Str = static_cast(*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 diff --git a/nixd/lib/Controller/CodeActions/RewriteString.h b/nixd/lib/Controller/CodeActions/RewriteString.h new file mode 100644 index 000000000..c7c1d592d --- /dev/null +++ b/nixd/lib/Controller/CodeActions/RewriteString.h @@ -0,0 +1,33 @@ +/// \file +/// \brief Code action for converting between string literal syntaxes. +/// +/// Transforms between double-quoted ("...") and indented (''...'') strings. + +#pragma once + +#include + +#include + +#include + +#include +#include + +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 &Actions); + +} // namespace nixd diff --git a/nixd/lib/meson.build b/nixd/lib/meson.build index a37168ee2..190bb89a0 100644 --- a/nixd/lib/meson.build +++ b/nixd/lib/meson.build @@ -14,6 +14,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/Completion.cpp', 'Controller/Configuration.cpp', diff --git a/nixd/tools/nixd/test/code-action/rewrite-string/empty-string.md b/nixd/tools/nixd/test/code-action/rewrite-string/empty-string.md new file mode 100644 index 000000000..46d3cd0b1 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/rewrite-string/empty-string.md @@ -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"} +``` diff --git a/nixd/tools/nixd/test/code-action/rewrite-string/escape-backslash.md b/nixd/tools/nixd/test/code-action/rewrite-string/escape-backslash.md new file mode 100644 index 000000000..973b3c339 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/rewrite-string/escape-backslash.md @@ -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"} +``` diff --git a/nixd/tools/nixd/test/code-action/rewrite-string/escape-newline.md b/nixd/tools/nixd/test/code-action/rewrite-string/escape-newline.md new file mode 100644 index 000000000..7e5ff3f82 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/rewrite-string/escape-newline.md @@ -0,0 +1,70 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +Test that newline escapes are properly converted between string formats. + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///escape-newline.nix +{ x = "hello\nworld"; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///escape-newline.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":6 + }, + "end":{ + "line":0, + "character":20 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +The converted indented string contains actual newlines (shown as \n in JSON). + +``` + CHECK: "id": 2, + CHECK: "result": [ + CHECK: "newText": "''hello\nworld''" + CHECK: "title": "Convert to indented string" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/rewrite-string/escape-quotes.md b/nixd/tools/nixd/test/code-action/rewrite-string/escape-quotes.md new file mode 100644 index 000000000..852bd9e91 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/rewrite-string/escape-quotes.md @@ -0,0 +1,72 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +Test that quote characters are properly escaped when converting formats. + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +Test converting indented string with double quotes to double-quoted string. + +```nix file:///escape-quotes.nix +{ x = ''say "hi"''; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///escape-quotes.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":6 + }, + "end":{ + "line":0, + "character":18 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +The double quotes inside should be escaped as \" + +``` + CHECK: "id": 2, + CHECK: "result": [ + CHECK: "newText": "\"say \\\"hi\\\"\"" + CHECK: "title": "Convert to double-quoted string" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/rewrite-string/escape-tab.md b/nixd/tools/nixd/test/code-action/rewrite-string/escape-tab.md new file mode 100644 index 000000000..23641d7a8 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/rewrite-string/escape-tab.md @@ -0,0 +1,70 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +Test that tab 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-tab.nix +{ x = "col1\tcol2"; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///escape-tab.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":6 + }, + "end":{ + "line":0, + "character":18 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +The tab escape becomes a literal tab character in the indented string (shown as \t in JSON). + +``` + CHECK: "id": 2, + CHECK: "result": [ + CHECK: "newText": "''col1\tcol2''" + CHECK: "title": "Convert to indented string" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/rewrite-string/interpolation.md b/nixd/tools/nixd/test/code-action/rewrite-string/interpolation.md new file mode 100644 index 000000000..1e9ee976e --- /dev/null +++ b/nixd/tools/nixd/test/code-action/rewrite-string/interpolation.md @@ -0,0 +1,68 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +Test that interpolations are preserved when converting string formats. + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///interpolation.nix +{ x = "hello ${name}!"; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///interpolation.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":6 + }, + "end":{ + "line":0, + "character":22 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, + CHECK: "result": [ + CHECK: "newText": "''hello ${name}!''" + CHECK: "title": "Convert to indented string" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/rewrite-string/not-string.md b/nixd/tools/nixd/test/code-action/rewrite-string/not-string.md new file mode 100644 index 000000000..799295a68 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/rewrite-string/not-string.md @@ -0,0 +1,71 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +Test that the rewrite string action is NOT offered when cursor is on a non-string node. + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///not-string.nix +{ x = 42; y = ./path; z = true; } +``` + +<-- textDocument/codeAction(2) + +Request code action on the integer literal (not a string). + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///not-string.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":6 + }, + "end":{ + "line":0, + "character":8 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +The rewrite string action should NOT be offered for non-string nodes. + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", + CHECK-NOT: "Convert to indented string" + CHECK-NOT: "Convert to double-quoted string" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/rewrite-string/to-dquote-interpolation.md b/nixd/tools/nixd/test/code-action/rewrite-string/to-dquote-interpolation.md new file mode 100644 index 000000000..7e2c29357 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/rewrite-string/to-dquote-interpolation.md @@ -0,0 +1,68 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +Test that interpolations in indented strings are preserved when converting to double-quoted. + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///to-dquote-interpolation.nix +{ x = ''hello ${name}!''; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///to-dquote-interpolation.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":6 + }, + "end":{ + "line":0, + "character":24 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, + CHECK: "result": [ + CHECK: "newText": "\"hello ${name}!\"" + CHECK: "title": "Convert to double-quoted string" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/rewrite-string/to-dquote-newline.md b/nixd/tools/nixd/test/code-action/rewrite-string/to-dquote-newline.md new file mode 100644 index 000000000..cf3f173df --- /dev/null +++ b/nixd/tools/nixd/test/code-action/rewrite-string/to-dquote-newline.md @@ -0,0 +1,73 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +Test that newlines in indented strings are properly escaped when converting to double-quoted. + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +The indented string contains an actual newline character between hello and world. + +```nix file:///to-dquote-newline.nix +{ x = ''hello +world''; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///to-dquote-newline.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":6 + }, + "end":{ + "line":1, + "character":7 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +The newline becomes an escape sequence \n in the double-quoted string (shown as \\n in JSON). + +``` + CHECK: "id": 2, + CHECK: "result": [ + CHECK: "newText": "\"hello\\nworld\"" + CHECK: "title": "Convert to double-quoted string" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/rewrite-string/to-dquote.md b/nixd/tools/nixd/test/code-action/rewrite-string/to-dquote.md new file mode 100644 index 000000000..f32a1ff31 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/rewrite-string/to-dquote.md @@ -0,0 +1,68 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +Test that "Convert to double-quoted string" action is offered for indented strings. + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///to-dquote.nix +{ x = ''hello''; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///to-dquote.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":6 + }, + "end":{ + "line":0, + "character":15 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, + CHECK: "result": [ + CHECK: "newText": "\"hello\"" + CHECK: "title": "Convert to double-quoted string" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/rewrite-string/to-indented.md b/nixd/tools/nixd/test/code-action/rewrite-string/to-indented.md new file mode 100644 index 000000000..ea94a7c9f --- /dev/null +++ b/nixd/tools/nixd/test/code-action/rewrite-string/to-indented.md @@ -0,0 +1,68 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +Test that "Convert to indented string" action is offered for double-quoted strings. + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///to-indented.nix +{ x = "hello"; } +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///to-indented.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":6 + }, + "end":{ + "line":0, + "character":13 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, + CHECK: "result": [ + CHECK: "newText": "''hello''" + CHECK: "title": "Convert to indented string" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` From 8bfec6a94f92f9764146b8419a99444e91343e73 Mon Sep 17 00:00:00 2001 From: takeokunn Date: Wed, 14 Jan 2026 09:55:07 +0900 Subject: [PATCH 2/2] fix(rewrite-string): skip rewriting strings used as attribute names - Add check to bypass rewriting when string is used as an attribute name - Prevents duplicate handling already covered by attribute name actions --- nixd/lib/Controller/CodeActions/RewriteString.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nixd/lib/Controller/CodeActions/RewriteString.cpp b/nixd/lib/Controller/CodeActions/RewriteString.cpp index 23de577d3..5b199b08b 100644 --- a/nixd/lib/Controller/CodeActions/RewriteString.cpp +++ b/nixd/lib/Controller/CodeActions/RewriteString.cpp @@ -15,7 +15,8 @@ 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) { +bool isIndentedString(llvm::StringRef Src, + const nixf::LexerCursorRange &Range) { size_t Offset = Range.lCur().offset(); if (Offset + 1 >= Src.size()) return false; @@ -111,6 +112,11 @@ void addRewriteStringAction(const nixf::Node &N, 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(*StringNode); // Determine current string type