Skip to content

Commit

Permalink
nixd: provide trigger character "." (#428)
Browse files Browse the repository at this point in the history
  • Loading branch information
inclyc authored Apr 18, 2024
2 parents 384592e + 47ff5f2 commit 258dcb0
Show file tree
Hide file tree
Showing 12 changed files with 244 additions and 32 deletions.
11 changes: 8 additions & 3 deletions libnixf/include/nixf/Basic/Nodes/Attrs.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,26 @@ class AttrName : public Node {

class AttrPath : public Node {
const std::vector<std::shared_ptr<AttrName>> Names;
// FIXME: workaround for just recording the trailing dot.
const std::shared_ptr<Misc> TrailingDot;

public:
AttrPath(LexerCursorRange Range, std::vector<std::shared_ptr<AttrName>> Names)
: Node(NK_AttrPath, Range), Names(std::move(Names)) {}
AttrPath(LexerCursorRange Range, std::vector<std::shared_ptr<AttrName>> Names,
std::shared_ptr<Misc> TrailingDot)
: Node(NK_AttrPath, Range), Names(std::move(Names)),
TrailingDot(std::move(TrailingDot)) {}

[[nodiscard]] const std::vector<std::shared_ptr<AttrName>> &names() const {
return Names;
}

[[nodiscard]] ChildVector children() const override {
ChildVector Children;
Children.reserve(Names.size());
Children.reserve(Names.size() + 1);
for (const auto &Name : Names) {
Children.push_back(Name.get());
}
Children.emplace_back(TrailingDot.get());
return Children;
}
};
Expand Down
5 changes: 4 additions & 1 deletion libnixf/src/Parse/ParseAttrs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ std::shared_ptr<AttrPath> Parser::parseAttrPath() {
assert(LastToken && "LastToken should be set after valid attrname");
std::vector<std::shared_ptr<AttrName>> AttrNames;
AttrNames.emplace_back(std::move(First));
std::shared_ptr<Misc> TrailingDot;
while (true) {
if (Token Tok = peek(); Tok.kind() == tok_dot) {
consume();
Expand All @@ -47,6 +48,7 @@ std::shared_ptr<AttrPath> Parser::parseAttrPath() {
D.fix("remove extra .").edit(TextEdit::mkRemoval(Tok.range()));
D.fix("insert dummy attrname")
.edit(TextEdit::mkInsertion(Tok.rCur(), R"("dummy")"));
TrailingDot = std::make_shared<Misc>(Tok.range());
continue;
}
AttrNames.emplace_back(std::move(Next));
Expand All @@ -55,7 +57,8 @@ std::shared_ptr<AttrPath> Parser::parseAttrPath() {
break;
}
return std::make_shared<AttrPath>(LexerCursorRange{Begin, LastToken->rCur()},
std::move(AttrNames));
std::move(AttrNames),
std::move(TrailingDot));
}

std::shared_ptr<Binding> Parser::parseBinding() {
Expand Down
3 changes: 2 additions & 1 deletion libnixf/src/Sema/SemaActions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,8 @@ std::shared_ptr<Expr> Sema::desugarInheritExpr(std::shared_ptr<AttrName> Name,
return std::make_shared<ExprVar>(Range, Name->id());

auto Path = std::make_shared<AttrPath>(
Range, std::vector<std::shared_ptr<AttrName>>{std::move(Name)});
Range, std::vector<std::shared_ptr<AttrName>>{std::move(Name)},
/*TrailingDot=*/nullptr);
return std::make_shared<ExprSelect>(Range, std::move(E), std::move(Path),
nullptr);
}
Expand Down
7 changes: 6 additions & 1 deletion nixd/include/nixd/Controller/Controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,21 @@
namespace nixd {

class Controller : public lspserver::LSPServer {
public:
using OptionMapTy = std::map<std::string, std::unique_ptr<AttrSetClientProc>>;

private:
std::unique_ptr<OwnedEvalClient> Eval;

// Use this worker for evaluating nixpkgs.
std::unique_ptr<AttrSetClientProc> NixpkgsEval;

std::mutex OptionsLock;
// Map of option providers.
//
// e.g. "nixos" -> nixos worker
// "home-manager" -> home-manager worker
std::map<std::string, std::unique_ptr<AttrSetClientProc>> Options;
OptionMapTy Options; // GUARDED_BY(OptionsLock)

AttrSetClientProc &nixpkgsEval() {
assert(NixpkgsEval);
Expand Down
27 changes: 27 additions & 0 deletions nixd/lib/Controller/AST.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,30 @@ nixd::getSelectAttrPath(const nixf::AttrName &N,
__builtin_unreachable();
}
}

std::optional<std::vector<std::string_view>>
nixd::findAttrPath(const nixf::Node &N, const nixf::ParentMapAnalysis &PM) {
if (const Node *Name = PM.upTo(N, Node::NK_AttrName)) {
std::vector<std::string_view> AttrPath;
if (const auto *Expr = PM.upExpr(N))
AttrPath = getValueAttrPath(*Expr, PM);
auto Select = getSelectAttrPath(static_cast<const AttrName &>(*Name), PM);
AttrPath.insert(AttrPath.end(), Select.begin(), Select.end());
assert(!AttrPath.empty());
return AttrPath;
}

// Consider this is an "extra" dot.
if (const Node *APNode = PM.upTo(N, Node::NK_AttrPath)) {
const auto &AP = static_cast<const AttrPath &>(*APNode);
std::vector<std::string_view> AttrPathVec;
if (const auto *Expr = PM.upExpr(N))
AttrPathVec = getValueAttrPath(*Expr, PM);
assert(!AP.names().empty());
auto Select = getSelectAttrPath(*AP.names().back(), PM);
AttrPathVec.insert(AttrPathVec.end(), Select.begin(), Select.end());
AttrPathVec.emplace_back("");
return AttrPathVec;
}
return std::nullopt;
}
7 changes: 7 additions & 0 deletions nixd/lib/Controller/AST.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include <nixf/Sema/ParentMap.h>
#include <nixf/Sema/VariableLookup.h>

#include <optional>

namespace nixd {

/// \brief Search up until there are some node associated with "EnvNode".
Expand Down Expand Up @@ -45,4 +47,9 @@ getValueAttrPath(const nixf::Node &N, const nixf::ParentMapAnalysis &PM);
std::vector<std::string_view>
getSelectAttrPath(const nixf::AttrName &N, const nixf::ParentMapAnalysis &PM);

/// \brief Heuristically find attrpath suitable for "attrpath" completion.
/// \returns non-empty std::vector attrpath.
std::optional<std::vector<std::string_view>>
findAttrPath(const nixf::Node &N, const nixf::ParentMapAnalysis &PM);

} // namespace nixd
43 changes: 22 additions & 21 deletions nixd/lib/Controller/Completion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,20 @@ class OptionCompletionProvider {
}
};

void completeAttrName(std::vector<std::string> Scope, std::string Prefix,
Controller::OptionMapTy &Options, bool CompletionSnippets,
std::vector<CompletionItem> &List) {
for (const auto &[Name, Provider] : Options) {
AttrSetClient *Client = Options.at(Name)->client();
if (!Client) [[unlikely]] {
elog("skipped client {0} as it is dead", Name);
continue;
}
OptionCompletionProvider OCP(*Client, Name, CompletionSnippets);
OCP.completeOptions(Scope, Prefix, List);
}
}

} // namespace

void Controller::onCompletion(const CompletionParams &Params,
Expand All @@ -246,32 +260,19 @@ void Controller::onCompletion(const CompletionParams &Params,
CompletionList List;
try {
const ParentMapAnalysis &PM = *TU->parentMap();

if (const Node *Name = PM.upTo(*Desc, Node::NK_AttrName)) {
// Complete attrpath.
std::vector<std::string_view> AttrPath;
if (const auto *Expr = PM.upExpr(*Desc))
AttrPath = getValueAttrPath(*Expr, PM);
auto Select =
getSelectAttrPath(static_cast<const AttrName &>(*Name), PM);
AttrPath.insert(AttrPath.end(), Select.begin(), Select.end());
assert(!AttrPath.empty());
if (auto AP = findAttrPath(*Desc, PM)) {
// Construct request.
std::vector<std::string> Scope;
Scope.reserve(AttrPath.size());
for (std::string_view Name : AttrPath) {
Scope.reserve(AP->size());
for (std::string_view Name : *AP) {
Scope.emplace_back(Name);
}
std::string Prefix = Scope.back();
Scope.pop_back();
for (const auto &[Name, Provider] : Options) {
AttrSetClient *Client = Options.at(Name)->client();
if (!Client) [[unlikely]] {
elog("skipped client {0} as it is dead", Name);
continue;
}
OptionCompletionProvider OCP(*Client, Name,
ClientCaps.CompletionSnippets);
OCP.completeOptions(Scope, Prefix, List.items);
{
std::lock_guard _(OptionsLock);
completeAttrName(std::move(Scope), std::move(Prefix), Options,
ClientCaps.CompletionSnippets, List.items);
}
} else {
const VariableLookupAnalysis &VLA = *TU->variableLookup();
Expand Down
1 change: 1 addition & 0 deletions nixd/lib/Controller/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ void Controller::updateConfig(Configuration NewConfig) {
}
}
if (!Config.options.empty()) {
std::lock_guard _(OptionsLock);
// Stop option workers that are not listed in config.
for (const auto &[Name, _] : Options) {
if (!Config.options.contains(Name)) {
Expand Down
11 changes: 7 additions & 4 deletions nixd/lib/Controller/LifeTime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ void Controller::
{"completionProvider",
Object{
{"resolveProvider", true},
{"triggerCharacters", {"."}},
}},
{"referencesProvider", true},
{"documentHighlightProvider", true},
Expand Down Expand Up @@ -150,11 +151,13 @@ void Controller::
}

// Launch nixos worker also.
startOption("nixos", Options["nixos"]);

if (AttrSetClient *Client = Options["nixos"]->client())
evalExprWithProgress(*Client, DefaultNixOSOptionsExpr, "nixos options");
{
std::lock_guard _(OptionsLock);
startOption("nixos", Options["nixos"]);

if (AttrSetClient *Client = Options["nixos"]->client())
evalExprWithProgress(*Client, DefaultNixOSOptionsExpr, "nixos options");
}
fetchConfig();
}

Expand Down
74 changes: 74 additions & 0 deletions nixd/tools/nixd/test/completion-dot-select.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# RUN: nixd --lit-test \
# RUN: --nixos-options-expr="{ foo.bar = { _type = \"option\"; }; }" \
# 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":"{ bar = 1; foo = foo.foo.; }"
}
}
}
```

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

```
CHECK: "id": 1,
CHECK-NEXT: "jsonrpc": "2.0",
CHECK-NEXT: "result": {
CHECK-NEXT: "isIncomplete": false,
CHECK-NEXT: "items": []
CHECK-NEXT: }
```


```json
{"jsonrpc":"2.0","method":"exit"}
```
82 changes: 82 additions & 0 deletions nixd/tools/nixd/test/completion-dot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# RUN: nixd --lit-test \
# RUN: --nixos-options-expr="{ foo.bar = { _type = \"option\"; }; }" \
# 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":"{ bar = 1; foo. }"
}
}
}
```

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

```
CHECK: "id": 1,
CHECK-NEXT: "jsonrpc": "2.0",
CHECK-NEXT: "result": {
CHECK-NEXT: "isIncomplete": false,
CHECK-NEXT: "items": [
CHECK-NEXT: {
CHECK-NEXT: "data": "",
CHECK-NEXT: "detail": "nixos",
CHECK-NEXT: "kind": 7,
CHECK-NEXT: "label": "foo",
CHECK-NEXT: "score": 0
CHECK-NEXT: }
CHECK-NEXT: ]
CHECK-NEXT: }
```


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

0 comments on commit 258dcb0

Please sign in to comment.