Skip to content

Commit

Permalink
nixd: lib + pkgs completion with "select", e.g. lib.gen|, `pkgs…
Browse files Browse the repository at this point in the history
….stdenv.mkDerivat|`
  • Loading branch information
inclyc committed Jul 16, 2024
1 parent b7b3a8e commit 2f2c80f
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 34 deletions.
10 changes: 8 additions & 2 deletions nixd/include/nixd/Protocol/AttrSet.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ constexpr inline std::string_view Exit = "exit";
using EvalExprParams = std::string;
using EvalExprResponse = std::optional<std::string>;

using AttrPathInfoParams = std::vector<std::string>;
/// \brief A list of strings that "select"s into a attribute set.
using Selector = std::vector<std::string>;

using PackageInfoParams = Selector;
using OptionInfoParams = Selector;

using AttrPathInfoParams = Selector;

struct PackageDescription {
std::optional<std::string> Name;
Expand All @@ -45,7 +51,7 @@ bool fromJSON(const llvm::json::Value &Params, PackageDescription &R,
llvm::json::Path P);

struct AttrPathCompleteParams {
std::vector<std::string> Scope;
Selector Scope;
/// \brief Search for packages prefixed with this "prefix"
std::string Prefix;
};
Expand Down
21 changes: 20 additions & 1 deletion nixd/lib/Controller/AST.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,31 @@ bool nixd::havePackageScope(const Node &N, const VariableLookupAnalysis &VLA,
continue;

// Hardcoded "pkgs", even more stupid.
if (static_cast<const ExprVar &>(*WithBody).id().name() == "pkgs")
if (static_cast<const ExprVar &>(*WithBody).id().name() == idioms::Pkgs)
return true;
}
return false;
}

nixd::Selector nixd::mkSelector(const nixf::AttrPath &AP,
Selector BaseSelector) {
const auto &Names = AP.names();
for (const auto &Name : Names) {
if (!Name->isStatic())
throw DynamicNameException();
BaseSelector.emplace_back(Name->staticName());
}
return BaseSelector;
}

nixd::Selector nixd::mkSelector(const nixf::ExprSelect &Select,
nixd::Selector BaseSelector) {
if (Select.path())
return nixd::mkSelector(*static_cast<const nixf::AttrPath *>(Select.path()),
std::move(BaseSelector));
return BaseSelector;
}

std::pair<std::vector<std::string>, std::string>
nixd::getScopeAndPrefix(const Node &N, const ParentMapAnalysis &PM) {
if (N.kind() != Node::NK_Identifer)
Expand Down
28 changes: 28 additions & 0 deletions nixd/lib/Controller/AST.h
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
/// \file
/// \brief This file declares some common analysis (tree walk) on the AST.

#include "nixd/Protocol/AttrSet.h"

#include <nixf/Sema/ParentMap.h>
#include <nixf/Sema/VariableLookup.h>

namespace nixd {

namespace idioms {

/// \brief Hardcoded name for "pkgs.xxx", or "with pkgs;"
///
/// Assume that the value of this variable have the same structure with `import
/// nixpkgs {}
constexpr inline std::string_view Pkgs = "pkgs";

/// \brief Hardcoded name for nixpkgs "lib"
///
/// Assume that the value of this variable is "nixpkgs lib".
/// e.g. lib.genAttrs.
constexpr inline std::string_view Lib = "lib";

} // namespace idioms

/// \brief Search up until there are some node associated with "EnvNode".
[[nodiscard]] const nixf::EnvNode *
upEnv(const nixf::Node &Desc, const nixf::VariableLookupAnalysis &VLA,
Expand All @@ -29,6 +47,16 @@ upEnv(const nixf::Node &Desc, const nixf::VariableLookupAnalysis &VLA,
std::pair<std::vector<std::string>, std::string>
getScopeAndPrefix(const nixf::Node &N, const nixf::ParentMapAnalysis &PM);

/// \brief The attrpath has a dynamic name, thus it cannot be trivially
/// transformed to "static" selector.
struct DynamicNameException : std::exception {};

/// \brief Construct a nixd::Selector from \p AP.
Selector mkSelector(const nixf::AttrPath &AP, Selector BaseSelector);

/// \brief Construct a nixd::Selector from \p Select.
Selector mkSelector(const nixf::ExprSelect &Select, Selector BaseSelector);

enum class FindAttrPathResult {
OK,
Inherit,
Expand Down
169 changes: 138 additions & 31 deletions nixd/lib/Controller/Completion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#include <semaphore>
#include <set>
#include <unordered_set>
#include <utility>

using namespace nixd;
Expand Down Expand Up @@ -123,7 +124,7 @@ class NixpkgsCompletionProvider {
}

/// \brief Ask nixpkgs provider, give us a list of names. (thunks)
void completePackages(std::vector<std::string> Scope, std::string Prefix,
void completePackages(const AttrPathCompleteParams &Params,
std::vector<CompletionItem> &Items) {
std::binary_semaphore Ready(0);
std::vector<std::string> Names;
Expand All @@ -138,12 +139,11 @@ class NixpkgsCompletionProvider {
Ready.release();
};
// Send request.
AttrPathCompleteParams Params{std::move(Scope), std::move(Prefix)};
NixpkgsClient.attrpathComplete(Params, std::move(OnReply));
Ready.acquire();
// Now we have "Names", use these to fill "Items".
for (const auto &Name : Names) {
if (Name.starts_with(Prefix)) {
if (Name.starts_with(Params.Prefix)) {
addItem(Items, CompletionItem{
.label = Name,
.kind = CompletionItemKind::Field,
Expand Down Expand Up @@ -265,6 +265,98 @@ void completeAttrName(const std::vector<std::string> &Scope,
}
}

void completeAttrPath(const Node &N, const ParentMapAnalysis &PM,
std::mutex &OptionsLock, Controller::OptionMapTy &Options,
bool Snippets,
std::vector<lspserver::CompletionItem> &Items) {
std::vector<std::string> Scope;
using PathResult = FindAttrPathResult;
auto R = findAttrPath(N, PM, Scope);
if (R == PathResult::OK) {
// Construct request.
std::string Prefix = Scope.back();
Scope.pop_back();
{
std::lock_guard _(OptionsLock);
completeAttrName(Scope, Prefix, Options, Snippets, Items);
}
}
}

void completeVarName(const VariableLookupAnalysis &VLA,
const ParentMapAnalysis &PM, const nixf::Node &N,
AttrSetClient &Client, std::vector<CompletionItem> &List) {
VLACompletionProvider VLAP(VLA);
VLAP.complete(N, List, PM);
if (havePackageScope(N, VLA, PM)) {
// Append it with nixpkgs completion
// FIXME: handle null nixpkgsClient()
NixpkgsCompletionProvider NCP(Client);
auto [Scope, Prefix] = getScopeAndPrefix(N, PM);
NCP.completePackages({Scope, Prefix}, List);
}
}

AttrPathCompleteParams mkParams(nixd::Selector Sel, bool IsComplete) {
if (IsComplete) {
return {
.Scope = std::move(Sel),
.Prefix = "",
};
}
std::string Back = std::move(Sel.back());
Sel.pop_back();
return {
.Scope = Sel,
.Prefix = std::move(Back),
};
}

/// \brief Complete a "select" expression.
/// \param IsComplete Whether or not the last element of the selector is
/// effectively incomplete.
/// e.g.
/// - incomplete: `lib.gen|`
/// - complete: `lib.attrset.|`
void completeSelect(const nixf::ExprSelect &Select, AttrSetClient &Client,
bool IsComplete, std::vector<CompletionItem> &List) {
// The base expr for selecting.
const nixf::Expr &BaseExpr = Select.expr();

// Determine that the name is one of special names interesting
// for nix language. If it is not a simple variable, skip this
// case.
if (BaseExpr.kind() != Node::NK_ExprVar) {
return;
}

const auto &Var = static_cast<const nixf::ExprVar &>(BaseExpr);

// See if the variable matches some idioms name we alreay know.
std::unordered_set<std::string_view> S{idioms::Pkgs, idioms::Lib};
auto It = S.find(Var.id().name());

// Unknown name, cannot deal with it.
if (It == S.end())
return;

// Ask nixpkgs provider to get idioms completion.
NixpkgsCompletionProvider NCP(Client);

// Construct the scope & prefix suitable for this "select".
Selector BaseSelector = It == S.find(idioms::Lib)
? Selector{std::string(idioms::Lib)}
: Selector{};
try {
Selector Sel = mkSelector(Select, std::move(BaseSelector));
NCP.completePackages(mkParams(Sel, IsComplete), List);
} catch (DynamicNameException &DE) {
lspserver::log(
"completion/select: skip because attribute path contains dynamic attr");
return;
}
}

} // namespace

void Controller::onCompletion(const CompletionParams &Params,
Expand All @@ -283,38 +375,53 @@ void Controller::onCompletion(const CompletionParams &Params,
Reply(CompletionList{});
return;
}
CompletionList List;
try {
const ParentMapAnalysis &PM = *TU->parentMap();
std::vector<std::string> Scope;
using PathResult = FindAttrPathResult;
auto R = findAttrPath(*Desc, PM, Scope);
if (R == PathResult::OK) {
// Construct request.
std::string Prefix = Scope.back();
Scope.pop_back();
{
std::lock_guard _(OptionsLock);
completeAttrName(Scope, Prefix, Options,
const nixf::Node &N = *Desc;
const ParentMapAnalysis &PM = *TU->parentMap();
const Node *MaybeUpExpr = PM.upExpr(N);
if (!MaybeUpExpr) {
// If there is no concrete expression containing the cursor
// Reply an empty list.
Reply(CompletionList{});
return;
}
// Otherwise, construct the completion list from a set of providers.
const Node &UpExpr = *MaybeUpExpr;
Reply([&]() -> CompletionList {
CompletionList List;
try {
switch (UpExpr.kind()) {
// In these cases, assume the cursor have "variable" scoping.
case Node::NK_ExprWith:
case Node::NK_ExprList:
case Node::NK_ExprVar: {
const VariableLookupAnalysis &VLA = *TU->variableLookup();
completeVarName(VLA, PM, N, *nixpkgsClient(), List.items);
break;
}
// A "select" expression. e.g.
// foo.a|
// foo.|
// foo.a.bar|
case Node::NK_ExprSelect: {
const auto &Select =
static_cast<const nixf::ExprSelect &>(UpExpr);
completeSelect(Select, *nixpkgsClient(), N.kind() == Node::NK_Dot,
List.items);
break;
}
case Node::NK_ExprAttrs: {
completeAttrPath(N, PM, OptionsLock, Options,
ClientCaps.CompletionSnippets, List.items);
break;
}
} else {
const VariableLookupAnalysis &VLA = *TU->variableLookup();
VLACompletionProvider VLAP(VLA);
VLAP.complete(*Desc, List.items, PM);
if (havePackageScope(*Desc, VLA, PM)) {
// Append it with nixpkgs completion
// FIXME: handle null nixpkgsClient()
NixpkgsCompletionProvider NCP(*nixpkgsClient());
auto [Scope, Prefix] = getScopeAndPrefix(*Desc, PM);
NCP.completePackages(Scope, Prefix, List.items);
default:
break;
}
} catch (ExceedSizeError &Err) {
List.isIncomplete = true;
}
// Next, add nixpkgs provided names.
} catch (ExceedSizeError &Err) {
List.isIncomplete = true;
}
Reply(std::move(List));
return List;
}());
}
}
};
Expand Down
77 changes: 77 additions & 0 deletions nixd/tools/nixd/test/completion-select-lib.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# RUN: nixd --lit-test \
# RUN: --nixpkgs-expr="{ lib.hello.meta.description = \"Very Nice\"; }" \
# RUN: < %s | FileCheck %s


<-- initialize(0)

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


<-- textDocument/didOpen


```json
{
"jsonrpc":"2.0",
"method":"textDocument/didOpen",
"params":{
"textDocument":{
"uri":"file:///completion.nix",
"languageId":"nix",
"version":1,
"text":"lib.hel"
}
}
}
```

```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/completion",
"params": {
"textDocument": {
"uri": "file:///completion.nix"
},
"position": {
"line": 0,
"character": 6
},
"context": {
"triggerKind": 1
}
}
}
```

```
CHECK: "isIncomplete": false,
CHECK-NEXT: "items": [
CHECK-NEXT: {
CHECK-NEXT: "data": "{\"Prefix\":\"hel\",\"Scope\":[\"lib\"]}",
CHECK-NEXT: "kind": 5,
CHECK-NEXT: "label": "hello",
CHECK-NEXT: "score": 0
CHECK-NEXT: }
CHECK-NEXT: ]
```


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

0 comments on commit 2f2c80f

Please sign in to comment.