Skip to content

Commit

Permalink
nixd/Eval: basic support for value metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
inclyc committed Jul 18, 2024
1 parent 334da32 commit bca99ec
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 36 deletions.
44 changes: 42 additions & 2 deletions nixd/include/nixd/Protocol/AttrSet.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@
#include <llvm/Support/JSON.h>
#include <lspserver/Protocol.h>

// https://github.com/NixOS/nix/issues/11136
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdocumentation"
#endif

#include <nix/value.hh>

#ifdef __clang__
#pragma clang diagnostic pop
#endif

namespace nixd {

namespace rpcMethod {
Expand Down Expand Up @@ -44,12 +56,40 @@ struct PackageDescription {
std::optional<std::string> Homepage;
};

using AttrPathInfoResponse = PackageDescription;

llvm::json::Value toJSON(const PackageDescription &Params);
bool fromJSON(const llvm::json::Value &Params, PackageDescription &R,
llvm::json::Path P);

/// \brief General metadata of all `nix::Value`s
struct ValueMeta {
/// \brief Type of this value.
int Type;

/// \brief Location of the value.
///
/// This presence of this value is determined by the nix evaluator.
/// In nix 2.19.x and later:
/// 1. It is available only for attribute sets and lambdas.
/// 2. There is no practical "range" information, only the starting point.
std::optional<lspserver::Location> Location;
};

llvm::json::Value toJSON(const ValueMeta &Params);
bool fromJSON(const llvm::json::Value &Params, ValueMeta &R,
llvm::json::Path P);

struct AttrPathInfoResponse {
/// \brief General value description
ValueMeta Meta;

/// \brief Package description of the attribute path, if available.
PackageDescription PackageDesc;
};

llvm::json::Value toJSON(const AttrPathInfoResponse &Params);
bool fromJSON(const llvm::json::Value &Params, AttrPathInfoResponse &R,
llvm::json::Path P);

struct AttrPathCompleteParams {
Selector Scope;
/// \brief Search for packages prefixed with this "prefix"
Expand Down
10 changes: 6 additions & 4 deletions nixd/lib/Controller/Completion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "lspserver/Protocol.h"

#include "nixd/Controller/Controller.h"
#include "nixd/Protocol/AttrSet.h"

#include <boost/asio/post.hpp>

Expand Down Expand Up @@ -105,7 +106,7 @@ class NixpkgsCompletionProvider {
void resolvePackage(std::vector<std::string> Scope, std::string Name,
CompletionItem &Item) {
std::binary_semaphore Ready(0);
PackageDescription Desc;
AttrPathInfoResponse Desc;
auto OnReply = [&Ready, &Desc](llvm::Expected<AttrPathInfoResponse> Resp) {
if (Resp)
Desc = *Resp;
Expand All @@ -115,12 +116,13 @@ class NixpkgsCompletionProvider {
NixpkgsClient.attrpathInfo(Scope, std::move(OnReply));
Ready.acquire();
// Format "detail" and document.
const PackageDescription &PD = Desc.PackageDesc;
Item.documentation = MarkupContent{
.kind = MarkupKind::Markdown,
.value = Desc.Description.value_or("") + "\n\n" +
Desc.LongDescription.value_or(""),
.value = PD.Description.value_or("") + "\n\n" +
PD.LongDescription.value_or(""),
};
Item.detail = Desc.Version.value_or("?");
Item.detail = PD.Version.value_or("?");
}

/// \brief Ask nixpkgs provider, give us a list of names. (thunks)
Expand Down
6 changes: 3 additions & 3 deletions nixd/lib/Controller/Definition.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "Convert.h"

#include "nixd/Controller/Controller.h"
#include "nixd/Protocol/AttrSet.h"

#include <boost/asio/post.hpp>

Expand Down Expand Up @@ -140,7 +141,7 @@ class NixpkgsDefinitionProvider {
Expected<lspserver::Location> resolvePackage(std::vector<std::string> Scope,
std::string Name) {
std::binary_semaphore Ready(0);
Expected<PackageDescription> Desc = error("not replied");
Expected<AttrPathInfoResponse> Desc = error("not replied");
auto OnReply = [&Ready, &Desc](llvm::Expected<AttrPathInfoResponse> Resp) {
if (Resp)
Desc = *Resp;
Expand All @@ -155,8 +156,7 @@ class NixpkgsDefinitionProvider {
if (!Desc)
return Desc.takeError();

const std::optional<std::string> &Position = Desc->Position;

const std::optional<std::string> &Position = Desc->PackageDesc.Position;
if (!Position)
return error("meta.position is not available for this package");

Expand Down
5 changes: 3 additions & 2 deletions nixd/lib/Controller/Hover.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "Convert.h"

#include "nixd/Controller/Controller.h"
#include "nixd/Protocol/AttrSet.h"

#include <boost/asio/post.hpp>

Expand Down Expand Up @@ -91,7 +92,7 @@ class NixpkgsHoverProvider {
std::optional<std::string> resolvePackage(std::vector<std::string> Scope,
std::string Name) {
std::binary_semaphore Ready(0);
std::optional<PackageDescription> Desc;
std::optional<AttrPathInfoResponse> Desc;
auto OnReply = [&Ready, &Desc](llvm::Expected<AttrPathInfoResponse> Resp) {
if (Resp)
Desc = *Resp;
Expand All @@ -106,7 +107,7 @@ class NixpkgsHoverProvider {
if (!Desc)
return std::nullopt;

return mkMarkdown(*Desc);
return mkMarkdown(Desc->PackageDesc);
}
};

Expand Down
4 changes: 2 additions & 2 deletions nixd/lib/Controller/InlayHints.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@ class NixpkgsInlayHintsProvider {
NixpkgsProvider.attrpathInfo({Name}, std::move(OnReply));
Ready.acquire();

if (R.Version) {
if (const std::optional<std::string> &Version = R.PackageDesc.Version) {
// Construct inlay hints.
InlayHint H{
.position = toLSPPosition(N->rCur()),
.label = ": " + *R.Version,
.label = ": " + *Version,
.kind = InlayHintKind::Designator,
.range = toLSPRange(N->range()),
};
Expand Down
76 changes: 55 additions & 21 deletions nixd/lib/Eval/AttrSetProvider.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#include "nixd/Eval/AttrSetProvider.h"
#include "nixd/Protocol/AttrSet.h"

#include "lspserver/Protocol.h"

#include <nix/attr-path.hh>
#include <nix/nixexpr.hh>
#include <nix/store-api.hh>
#include <nixt/Value.h>

Expand All @@ -24,15 +28,46 @@ void fillString(nix::EvalState &State, nix::Value &V,
}
}

void fillPackageDescription(nix::EvalState &State, nix::Value &Package,
PackageDescription &R) {
/// Describe the value as if \p Package is actually an nixpkgs package.
PackageDescription describePackage(nix::EvalState &State, nix::Value &Package) {
PackageDescription R;
fillString(State, Package, {"name"}, R.Name);
fillString(State, Package, {"pname"}, R.PName);
fillString(State, Package, {"version"}, R.Version);
fillString(State, Package, {"meta", "description"}, R.Description);
fillString(State, Package, {"meta", "longDescription"}, R.LongDescription);
fillString(State, Package, {"meta", "position"}, R.Position);
fillString(State, Package, {"meta", "homepage"}, R.Homepage);
return R;
}

std::optional<Location> locationOf(nix::PosTable &PTable, nix::Value &V) {
nix::PosIdx P = V.determinePos(nix::noPos);
if (!P)
return std::nullopt;

nix::Pos NixPos = PTable[P];
const auto *SP = std::get_if<nix::SourcePath>(&NixPos.origin);

if (!SP)
return std::nullopt;

Position LPos = {
.line = static_cast<int>(NixPos.line - 1),
.character = static_cast<int>(NixPos.column - 1),
};

return Location{
.uri = URIForFile::canonicalize(SP->path.abs(), SP->path.abs()),
.range = {LPos, LPos},
};
}

ValueMeta metadataOf(nix::EvalState &State, nix::Value &V) {
return {
.Type = V.type(true),
.Location = locationOf(State.positions, V),
};
}

void fillUnsafeGetAttrPosLocation(nix::EvalState &State, nix::Value &V,
Expand Down Expand Up @@ -158,26 +193,25 @@ void AttrSetProvider::onEvalExpr(
void AttrSetProvider::onAttrPathInfo(
const AttrPathInfoParams &AttrPath,
lspserver::Callback<AttrPathInfoResponse> Reply) {
try {
if (AttrPath.empty()) {
Reply(error("attrpath is empty!"));
return;
}

nix::Value &Package = nixt::selectStrings(state(), Nixpkgs, AttrPath);

AttrPathInfoResponse R;
fillPackageDescription(state(), Package, R);
using RespT = AttrPathInfoResponse;
Reply([&]() -> llvm::Expected<RespT> {
try {
if (AttrPath.empty()) {
return error("attrpath is empty!");
}

Reply(std::move(R));
return;
} catch (const nix::BaseError &Err) {
Reply(error(Err.info().msg.str()));
return;
} catch (const std::exception &Err) {
Reply(error(Err.what()));
return;
}
nix::Value &V = nixt::selectStrings(state(), Nixpkgs, AttrPath);
state().forceValue(V, nix::noPos);
return RespT{
.Meta = metadataOf(state(), V),
.PackageDesc = describePackage(state(), V),
};
} catch (const nix::BaseError &Err) {
return error(Err.info().msg.str());
} catch (const std::exception &Err) {
return error(Err.what());
}
}());
}

void AttrSetProvider::onAttrPathComplete(
Expand Down
32 changes: 32 additions & 0 deletions nixd/lib/Protocol/AttrSet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,38 @@ bool nixd::fromJSON(const llvm::json::Value &Params, PackageDescription &R,
;
}

Value nixd::toJSON(const ValueMeta &Params) {
return Object{
{"Type", Params.Type},
{"Location", Params.Location},
};
}

bool nixd::fromJSON(const llvm::json::Value &Params, ValueMeta &R,
llvm::json::Path P) {
ObjectMapper O(Params, P);
return O //
&& O.map("Type", R.Type) //
&& O.mapOptional("Location", R.Location) //
;
}

Value nixd::toJSON(const AttrPathInfoResponse &Params) {
return Object{
{"Meta", Params.Meta},
{"PackageDesc", Params.PackageDesc},
};
}

bool nixd::fromJSON(const llvm::json::Value &Params, AttrPathInfoResponse &R,
llvm::json::Path P) {
ObjectMapper O(Params, P);
return O //
&& O.map("Meta", R.Meta) //
&& O.mapOptional("PackageDesc", R.PackageDesc) //
;
}

Value nixd::toJSON(const AttrPathCompleteParams &Params) {
return Object{{"Scope", Params.Scope}, {"Prefix", Params.Prefix}};
}
Expand Down
10 changes: 8 additions & 2 deletions nixd/tools/nixd-attrset-eval/test/attrs-info.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,16 @@
```

```
CHECK: "id": 1,
CHECK: "id": 1,
CHECK-NEXT: "jsonrpc": "2.0",
CHECK-NEXT: "result": {
CHECK-NEXT: "Description": "A program that produces a familiar, friendly greeting",
CHECK-NEXT: "Meta": {
CHECK-NEXT: "Location": null,
CHECK-NEXT: "Type": 7
CHECK-NEXT: },
CHECK-NEXT: "PackageDesc": {
CHECK-NEXT: "Description": "A program that produces a familiar, friendly greeting",
CHECK-NEXT: "Homepage": null,
```

```json
Expand Down

0 comments on commit bca99ec

Please sign in to comment.