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
4 changes: 4 additions & 0 deletions nixd/docs/editors/nvim-lsp.nix
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ let
relativenumber = true,
scrolloff = 8,
updatetime = 50, -- faster completion (4000ms default)
foldmethod = "expr",
foldexpr = "v:lua.vim.lsp.foldexpr()",
foldlevel = 99,
foldenable = true,
}

for k, v in pairs(options) do
Expand Down
4 changes: 4 additions & 0 deletions nixd/include/nixd/Controller/Controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ class Controller : public lspserver::LSPServer {
const lspserver::DocumentSymbolParams &Params,
lspserver::Callback<std::vector<lspserver::DocumentSymbol>> Reply);

void onFoldingRange(
const lspserver::FoldingRangeParams &Params,
lspserver::Callback<std::vector<lspserver::FoldingRange>> Reply);

void onSemanticTokens(const lspserver::SemanticTokensParams &Params,
lspserver::Callback<lspserver::SemanticTokens> Reply);

Expand Down
154 changes: 154 additions & 0 deletions nixd/lib/Controller/FoldingRange.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/// \file
/// \brief Implementation of [Folding Range].
/// [Folding Range]:
/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_foldingRange

#include "CheckReturn.h"
#include "Convert.h"

#include "nixd/Controller/Controller.h"

#include <boost/asio/post.hpp>
#include <lspserver/Logger.h>
#include <lspserver/Protocol.h>
#include <nixf/Basic/Nodes/Attrs.h>
#include <nixf/Basic/Nodes/Expr.h>
#include <nixf/Basic/Nodes/Lambda.h>
#include <nixf/Basic/Nodes/Simple.h>

using namespace nixd;
using namespace lspserver;
using namespace nixf;

namespace {

/// Maximum recursion depth to prevent stack overflow on deeply nested ASTs.
constexpr size_t MaxRecursionDepth = 256;

/// Check if the range spans multiple lines (2+ lines required for folding).
bool isMultiLine(const lspserver::Range &R) {
return R.start.line < R.end.line;
}

/// Create a FoldingRange from an LSP Range with "region" kind.
FoldingRange toFoldingRange(const lspserver::Range &R) {
FoldingRange FR;
FR.startLine = R.start.line;
FR.startCharacter = R.start.character;
FR.endLine = R.end.line;
FR.endCharacter = R.end.character;
FR.kind = FoldingRange::REGION_KIND;
return FR;
}

/// Add a folding range if the node spans multiple lines.
void addFoldingRange(const Node &N, std::vector<FoldingRange> &Ranges,
llvm::StringRef Src) {
auto R = toLSPRange(Src, N.range());
if (isMultiLine(R))
Ranges.emplace_back(toFoldingRange(R));
}

/// Collect folding ranges from AST nodes recursively.
/// \param AST The AST node to process
/// \param Ranges Output vector to collect folding ranges
/// \param Src Source text for position conversion
/// \param Depth Current recursion depth (for stack overflow protection)
void collectFoldingRanges(const Node *AST, std::vector<FoldingRange> &Ranges,
llvm::StringRef Src, size_t Depth = 0) {
if (!AST || Depth >= MaxRecursionDepth)
return;

switch (AST->kind()) {
case Node::NK_ExprAttrs: {
addFoldingRange(*AST, Ranges, Src);
const auto &Attrs = static_cast<const ExprAttrs &>(*AST);
if (Attrs.binds()) {
for (const Node *Ch : Attrs.binds()->children())
collectFoldingRanges(Ch, Ranges, Src, Depth + 1);
}
break;
}
case Node::NK_ExprList: {
addFoldingRange(*AST, Ranges, Src);
for (const Node *Ch : AST->children())
collectFoldingRanges(Ch, Ranges, Src, Depth + 1);
break;
}
case Node::NK_ExprLambda: {
addFoldingRange(*AST, Ranges, Src);
const auto &Lambda = static_cast<const ExprLambda &>(*AST);
if (const auto *Body = Lambda.body())
collectFoldingRanges(Body, Ranges, Src, Depth + 1);
break;
}
case Node::NK_ExprLet: {
addFoldingRange(*AST, Ranges, Src);
const auto &Let = static_cast<const ExprLet &>(*AST);
if (const auto *Attrs = Let.attrs())
collectFoldingRanges(Attrs, Ranges, Src, Depth + 1);
if (const auto *E = Let.expr())
collectFoldingRanges(E, Ranges, Src, Depth + 1);
break;
}
case Node::NK_ExprWith: {
addFoldingRange(*AST, Ranges, Src);
const auto &With = static_cast<const ExprWith &>(*AST);
if (const auto *W = With.with())
collectFoldingRanges(W, Ranges, Src, Depth + 1);
if (const auto *E = With.expr())
collectFoldingRanges(E, Ranges, Src, Depth + 1);
break;
}
case Node::NK_ExprIf: {
addFoldingRange(*AST, Ranges, Src);
const auto &If = static_cast<const ExprIf &>(*AST);
if (const auto *Cond = If.cond())
collectFoldingRanges(Cond, Ranges, Src, Depth + 1);
if (const auto *Then = If.then())
collectFoldingRanges(Then, Ranges, Src, Depth + 1);
if (const auto *Else = If.elseExpr())
collectFoldingRanges(Else, Ranges, Src, Depth + 1);
break;
}
case Node::NK_ExprString:
// Multiline strings are foldable regions
addFoldingRange(*AST, Ranges, Src);
break;
default:
// Other node types may contain foldable children, recurse into them
for (const Node *Ch : AST->children())
collectFoldingRanges(Ch, Ranges, Src, Depth + 1);
break;
}
}

} // namespace

/// \brief Handle textDocument/foldingRange LSP request.
///
/// Collects foldable regions from the document's AST including attribute sets,
/// lists, lambdas, let/with/if expressions, and multiline strings.
///
/// \param Params Request parameters containing the document URI
/// \param Reply Callback to return the list of folding ranges
void Controller::onFoldingRange(const FoldingRangeParams &Params,
Callback<std::vector<FoldingRange>> Reply) {
using CheckTy = std::vector<FoldingRange>;
auto Action = [Reply = std::move(Reply), URI = Params.textDocument.uri,
this]() mutable {
return Reply([&]() -> llvm::Expected<CheckTy> {
const auto TU = CheckDefault(getTU(URI.file().str()));
const auto AST = CheckDefault(getAST(*TU));
try {
auto Ranges = std::vector<FoldingRange>();
collectFoldingRanges(AST.get(), Ranges, TU->src());
return Ranges;
} catch (std::exception &E) {
elog("textDocument/foldingRange failed: {0}", E.what());
return CheckTy{};
}
}());
};
boost::asio::post(Pool, std::move(Action));
}
1 change: 1 addition & 0 deletions nixd/lib/Controller/LifeTime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ void Controller::
{"definitionProvider", true},
{"documentLinkProvider", Object{}},
{"documentSymbolProvider", true},
{"foldingRangeProvider", true},
{"inlayHintProvider", true},
{"completionProvider",
Object{
Expand Down
2 changes: 2 additions & 0 deletions nixd/lib/Controller/Support.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ Controller::Controller(std::unique_ptr<lspserver::InboundPort> In,
&Controller::onDefinition);
Registry.addMethod("textDocument/documentSymbol", this,
&Controller::onDocumentSymbol);
Registry.addMethod("textDocument/foldingRange", this,
&Controller::onFoldingRange);
Registry.addMethod("textDocument/semanticTokens/full", this,
&Controller::onSemanticTokens);
Registry.addMethod("textDocument/inlayHint", this, &Controller::onInlayHint);
Expand Down
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/DocumentHighlight.cpp',
'Controller/DocumentLink.cpp',
'Controller/DocumentSymbol.cpp',
'Controller/FoldingRange.cpp',
'Controller/FindReferences.cpp',
'Controller/Format.cpp',
'Controller/Hover.cpp',
Expand Down
90 changes: 90 additions & 0 deletions nixd/tools/nixd/test/folding-range/basic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# RUN: nixd --lit-test < %s | FileCheck %s

Test basic folding range functionality for attribute sets and lists.

<-- initialize(0)

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

<-- textDocument/didOpen

```nix file:///test.nix
{
# Multi-line attribute set
attrs = {
x = 1;
y = 2;
};

# Multi-line list
list = [
1
2
];
}
```

<-- textDocument/foldingRange(2)

```json
{
"jsonrpc":"2.0",
"id":2,
"method":"textDocument/foldingRange",
"params":{
"textDocument":{
"uri":"file:///test.nix"
}
}
}
```

We expect folding ranges for:
1. Outer attribute set (lines 0-12)
2. Inner "attrs" attribute set (lines 2-5)
3. List (lines 8-11)

```
CHECK: "id": 2,
CHECK-NEXT: "jsonrpc": "2.0",
CHECK-NEXT: "result": [
CHECK-NEXT: {
CHECK-NEXT: "endCharacter": 1,
CHECK-NEXT: "endLine": 12,
CHECK-NEXT: "kind": "region",
CHECK-NEXT: "startLine": 0
CHECK-NEXT: },
CHECK-NEXT: {
CHECK-NEXT: "endCharacter": 3,
CHECK-NEXT: "endLine": 5,
CHECK-NEXT: "kind": "region",
CHECK-NEXT: "startCharacter": 10,
CHECK-NEXT: "startLine": 2
CHECK-NEXT: },
CHECK-NEXT: {
CHECK-NEXT: "endCharacter": 3,
CHECK-NEXT: "endLine": 11,
CHECK-NEXT: "kind": "region",
CHECK-NEXT: "startCharacter": 9,
CHECK-NEXT: "startLine": 8
CHECK-NEXT: }
CHECK-NEXT: ]
CHECK-NEXT:}
```

```json
{"jsonrpc":"2.0","method":"exit"}
```
103 changes: 103 additions & 0 deletions nixd/tools/nixd/test/folding-range/let-lambda.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# RUN: nixd --lit-test < %s | FileCheck %s

Test folding range for let expressions and lambda functions.

<-- initialize(0)

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

<-- textDocument/didOpen

```nix file:///test.nix
let
x = 1;
y = 2;
in
{
fn = { pkgs, lib, ... }:
{
name = "test";
};
}
```

<-- textDocument/foldingRange(2)

```json
{
"jsonrpc":"2.0",
"id":2,
"method":"textDocument/foldingRange",
"params":{
"textDocument":{
"uri":"file:///test.nix"
}
}
}
```

We expect folding ranges for:
1. Let expression (lines 0-9)
2. Let bindings (lines 1-2)
3. Attribute set in let body (lines 4-9)
4. Lambda expression (lines 5-8)
5. Inner attribute set in lambda body (lines 6-8)

```
CHECK: "id": 2,
CHECK-NEXT: "jsonrpc": "2.0",
CHECK-NEXT: "result": [
CHECK-NEXT: {
CHECK-NEXT: "endCharacter": 1,
CHECK-NEXT: "endLine": 9,
CHECK-NEXT: "kind": "region",
CHECK-NEXT: "startLine": 0
CHECK-NEXT: },
CHECK-NEXT: {
CHECK-NEXT: "endCharacter": 8,
CHECK-NEXT: "endLine": 2,
CHECK-NEXT: "kind": "region",
CHECK-NEXT: "startCharacter": 2,
CHECK-NEXT: "startLine": 1
CHECK-NEXT: },
CHECK-NEXT: {
CHECK-NEXT: "endCharacter": 1,
CHECK-NEXT: "endLine": 9,
CHECK-NEXT: "kind": "region",
CHECK-NEXT: "startLine": 4
CHECK-NEXT: },
CHECK-NEXT: {
CHECK-NEXT: "endCharacter": 3,
CHECK-NEXT: "endLine": 8,
CHECK-NEXT: "kind": "region",
CHECK-NEXT: "startCharacter": 7,
CHECK-NEXT: "startLine": 5
CHECK-NEXT: },
CHECK-NEXT: {
CHECK-NEXT: "endCharacter": 3,
CHECK-NEXT: "endLine": 8,
CHECK-NEXT: "kind": "region",
CHECK-NEXT: "startCharacter": 2,
CHECK-NEXT: "startLine": 6
CHECK-NEXT: }
CHECK-NEXT: ]
CHECK-NEXT:}
```

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

Loading