Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixd: lib + pkgs completion with "select", e.g. lib.gen|, pkgs.stdenv.mkDerivat| #544

Merged
merged 1 commit into from
Jul 16, 2024
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
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
Loading