Skip to content

Commit

Permalink
nixd/Server: semantic completion (#225)
Browse files Browse the repository at this point in the history
  • Loading branch information
inclyc authored Jul 30, 2023
2 parents 71833c1 + 714f356 commit 64d47ca
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 64 deletions.
31 changes: 31 additions & 0 deletions nixd/include/nixd/AST/ParseAST.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,25 @@ class ParseAST {
using Symbols = std::vector<lspserver::DocumentSymbol>;
using Links = std::vector<lspserver::DocumentLink>;

enum class LocationContext {

/// Expecting an attr name, not values.
/// {
/// |
/// ^
/// }
AttrName,

/// Expecting a nix value.
/// {
/// a = |
/// ^
/// }
Value,

Unknown
};

protected:
std::unique_ptr<ParseData> Data;
std::map<const nix::Expr *, const nix::Expr *> ParentMap;
Expand Down Expand Up @@ -76,6 +95,16 @@ class ParseAST {

[[nodiscard]] RangeIdx nPairIdx(nix::PosIdx P) const { return {P, end(P)}; }

[[nodiscard]] bool contains(nix::PosIdx P,
const lspserver::Position &Pos) const {
try {
lspserver::Range Range = nPair(P);
return Range.contains(Pos);
} catch (std::out_of_range &) {
return false;
}
}

void prepareDefRef();

std::optional<Definition> searchDef(const nix::ExprVar *Var) const;
Expand Down Expand Up @@ -190,5 +219,7 @@ class ParseAST {

[[nodiscard]] std::vector<lspserver::CompletionItem>
completion(const lspserver::Position &Pos) const;

[[nodiscard]] LocationContext getContext(lspserver::Position Pos) const;
};
} // namespace nixd
2 changes: 1 addition & 1 deletion nixd/include/nixd/Server/Controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ class Controller : public lspserver::LSPServer {
lspserver::Callback<lspserver::Hover>);

void onCompletion(const lspserver::CompletionParams &,
lspserver::Callback<lspserver::CompletionList>);
lspserver::Callback<llvm::json::Value>);

void onFormat(const lspserver::DocumentFormattingParams &,
lspserver::Callback<std::vector<lspserver::TextEdit>>);
Expand Down
36 changes: 36 additions & 0 deletions nixd/lib/AST/ParseAST.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#include "nixd/AST/ParseAST.h"
#include "nixd/Expr/Expr.h"

#include "lspserver/Protocol.h"

#include <nix/nixexpr.hh>
#include <nix/symbol-table.hh>

#include <optional>
#include <stdexcept>

namespace nixd {

Expand Down Expand Up @@ -299,4 +302,37 @@ ParseAST::completion(const lspserver::Position &Pos) const {
return Items;
}

namespace {
struct AttrDefVisitor : RecursiveASTVisitor<AttrDefVisitor> {
const ParseAST &AST;
const lspserver::Position &Pos;
bool InAttrDef;
bool visitExprAttrs(const nix::ExprAttrs *E) {
// TODO: std::ranges::all_of
for (const auto &[_, Def] : E->attrs) {
if (AST.contains(Def.pos, Pos)) {
InAttrDef = true;
// Stop traversing
return false;
}
}
return true;
}
};

} // namespace

ParseAST::LocationContext ParseAST::getContext(lspserver::Position Pos) const {
AttrDefVisitor V{.AST = *this, .Pos = Pos};
V.traverseExpr(root());
if (V.InAttrDef)
return LocationContext::AttrName;
if (const auto *E = lookupContainMin(Pos)) {
const auto *EAttrs = dynamic_cast<const nix::ExprAttrs *>(E);
if (!EAttrs)
return LocationContext::Value;
}
return LocationContext::Unknown;
}

} // namespace nixd
1 change: 0 additions & 1 deletion nixd/lib/Parser/Parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,6 @@ binds
| binds attrpath error {
$$ = $1;
auto *Err = data->ctx.record(new ExprError);
data->locations[Err] = CUR_POS;
addAttr($$, std::move(*$2), Err, makeCurPos(@2, data), *data);
data->locations[$$] = CUR_POS;
}
Expand Down
137 changes: 76 additions & 61 deletions nixd/lib/Server/Controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -544,76 +544,91 @@ void Controller::onHover(const lspserver::TextDocumentPositionParams &Params,
boost::asio::post(Pool, std::move(Task));
}

void Controller::onCompletion(
const lspserver::CompletionParams &Params,
lspserver::Callback<lspserver::CompletionList> Reply) {
auto EnableOption = Config.options.enable;
using RTy = lspserver::CompletionList;
using namespace lspserver;
constexpr auto Method = "nixd/ipc/textDocument/completion";
auto Task = [=, Reply = std::move(Reply), this]() mutable {
auto Resp = askWC<RTy>(Method, Params, {EvalWorkers, EvalWorkerLock, 2e6});
std::optional<RTy> R;
if (!Resp.empty())
R = std::move(Resp.back());
else {
// Statically construct the completion list.
std::binary_semaphore Smp(0);
try {
auto Path = Params.textDocument.uri.file();
auto Action = [&Smp, &Resp, Pos = Params.position](
const ParseAST &AST, ASTManager::VersionTy Version) {
auto Items = AST.completion(Pos);
Resp.emplace_back(CompletionList{false, Items});
Smp.release();
};
if (auto Draft = DraftMgr.getDraft(Path)) {
auto Version =
EvalDraftStore::decodeVersion(Draft->Version).value_or(0);
ASTMgr.withAST(Path.str(), Version, std::move(Action));
Smp.acquire();
}
} catch (std::exception &E) {
lspserver::elog("completion/parseAST: {0}", stripANSI(E.what()));
} catch (...) {
void Controller::onCompletion(const lspserver::CompletionParams &Params,
lspserver::Callback<llvm::json::Value> Reply) {
// Statically construct the completion list.
std::binary_semaphore Smp(0);
auto Path = Params.textDocument.uri.file();
auto Action = [&Smp, &Params, &Reply,
this](const ParseAST &AST,
ASTManager::VersionTy Version) mutable {
using PL = ParseAST::LocationContext;
using List = lspserver::CompletionList;
ReplyRAII<llvm::json::Value> RR(std::move(Reply));

auto CompletionFromOptions = [&, this]() -> std::optional<List> {
bool OptionsEnabled;
{
std::lock_guard _(ConfigLock);
OptionsEnabled = Config.options.enable;
}
}
if (OptionsEnabled) {
ipc::AttrPathParams APParams;

if (EnableOption) {
ipc::AttrPathParams APParams;
if (Params.context.triggerCharacter == ".") {
// Get nixpkgs options
// TODO: split this in AST, use AST-based attrpath construction.
auto Code = llvm::StringRef(
*DraftMgr.getDraft(Params.textDocument.uri.file())->Contents);
auto ExpectedPosition = positionToOffset(Code, Params.position);

if (Params.context.triggerCharacter == ".") {
// Get nixpkgs options
auto Code = llvm::StringRef(
*DraftMgr.getDraft(Params.textDocument.uri.file())->Contents);
auto ExpectedPosition = positionToOffset(Code, Params.position);
// get the attr path
auto TruncateBackCode = Code.substr(0, ExpectedPosition.get());

// get the attr path
auto TruncateBackCode = Code.substr(0, ExpectedPosition.get());
auto [_, AttrPath] = TruncateBackCode.rsplit(" ");
APParams.Path = AttrPath.str();
}

auto [_, AttrPath] = TruncateBackCode.rsplit(" ");
APParams.Path = AttrPath.str();
auto Resp = askWC<lspserver::CompletionList>(
"nixd/ipc/textDocument/completion/options", APParams,
{OptionWorkers, OptionWorkerLock, 1e5});
if (!Resp.empty())
return Resp.back();
}
return std::nullopt;
};

auto RespOption = askWC<lspserver::CompletionList>(
"nixd/ipc/textDocument/completion/options", APParams,
{OptionWorkers, OptionWorkerLock, 1e5});
if (!RespOption.empty()) {
if (R) {
// Merge response and option response.
auto O = RespOption.back().items;
R->items.insert(R->items.end(), O.begin(), O.end());
} else {
R = std::move(RespOption.back());
}
}
auto CompletionFromEval = [&, this]() -> std::optional<List> {
constexpr auto Method = "nixd/ipc/textDocument/completion";
auto Resp =
askWC<List>(Method, Params, {EvalWorkers, EvalWorkerLock, 2e6});
std::optional<List> R;
if (!Resp.empty())
return std::move(Resp.back());
return std::nullopt;
};

switch (AST.getContext(Params.position)) {
case (PL::AttrName): {
// Requested completion on an attribute name, not values.
RR.Response = CompletionFromOptions();
break;
}
if (R)
Reply(std::move(*R));
else
Reply(RTy{});
case (PL::Value): {
RR.Response = CompletionFromEval();
break;
}
case (PL::Unknown):
List L;
if (auto LOptions = CompletionFromOptions())
L.items.insert(L.items.end(), (*LOptions).items.begin(),
(*LOptions).items.end());
if (auto LEval = CompletionFromEval())
L.items.insert(L.items.end(), (*LEval).items.begin(),
(*LEval).items.end());
L.isIncomplete = true;
RR.Response = std::move(L);
}
Smp.release();
};
boost::asio::post(Pool, std::move(Task));

if (auto Draft = DraftMgr.getDraft(Path)) {
auto Version = EvalDraftStore::decodeVersion(Draft->Version).value_or(0);
ASTMgr.withAST(Path.str(), Version, std::move(Action));
Smp.acquire();
} else {
Reply(lspserver::error("requested completion list on unknown draft path"));
}
}

void Controller::onRename(const lspserver::RenameParams &Params,
Expand Down
32 changes: 32 additions & 0 deletions nixd/test/ast.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "nixd/AST/EvalAST.h"
#include "nixd/AST/ParseAST.h"

#include "nixd/Parser/Parser.h"
#include "nixutil.h"

#include <nix/canon-path.hh>
Expand Down Expand Up @@ -38,6 +39,37 @@ TEST(AST, lookupEnd) {
}
}

TEST(AST, LocationContext) {
std::string NixSrc = R"(
{
a = {
# ^AttrName
b = 1;
# ^Value
};
# ^Unknown
d = {
z = {
y = 1;
};
};
list = [ ];
# ^Value
}
)";
InitNix INix;
auto State = INix.getDummyState();
ParseAST A(parse(NixSrc, nix::CanonPath("foo"), nix::CanonPath("/"), *State));
ASSERT_EQ(A.getContext({2, 2}), ParseAST::LocationContext::AttrName);
ASSERT_EQ(A.getContext({4, 8}), ParseAST::LocationContext::Value);
ASSERT_EQ(A.getContext({8, 8}), ParseAST::LocationContext::Unknown);
ASSERT_EQ(A.getContext({17, 10}), ParseAST::LocationContext::Value);
}

TEST(AST, lookupContainMin) {
std::string NixSrc = R"(
{
Expand Down
2 changes: 1 addition & 1 deletion nixd/tools/nixd/test/completion-attrset.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Complete this file:
CHECK: "id": 1,
CHECK-NEXT: "jsonrpc": "2.0",
CHECK-NEXT: "result": {
CHECK-NEXT: "isIncomplete": false,
CHECK-NEXT: "isIncomplete": true,
CHECK-NEXT: "items": [
CHECK-NEXT: {
CHECK-NEXT: "kind": 5,
Expand Down

0 comments on commit 64d47ca

Please sign in to comment.