Skip to content

Commit

Permalink
nixd: introduce attrset provider (#394)
Browse files Browse the repository at this point in the history
  • Loading branch information
inclyc authored Apr 11, 2024
1 parent d5b81ec commit 7837528
Show file tree
Hide file tree
Showing 11 changed files with 558 additions and 0 deletions.
1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
export PATH="${pkgs.clang-tools}/bin:$PATH"
export NIX_DEBUG_INFO_DIRS=${nix.debug}/lib/debug
export NIX_SRC=${nix.src}
export NIX_PATH=nixpkgs=${nixpkgs}
'';
hardeningDisable = [ "fortify" ];
};
Expand Down
60 changes: 60 additions & 0 deletions nixd/include/nixd/Eval/AttrSetProvider.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/// \file
/// \brief Dedicated worker for evaluating attrset.
///
/// Motivation: eval things in attrset (e.g. nixpkgs). (packages, functions ...)
///
/// Observation:
/// 1. Fast: eval (import <nixpkgs> { }).
/// 2. Slow: eval a whole NixOS config until some node is being touched.
///
/// This worker is designed to answer "packages"/"functions" in nixpkgs, but not
/// limited to it.
///
/// For time-saving:
/// Once a value is evaluated. It basically assume it will not change.
/// That is, any workspace editing will not invalidate the value.
///

#pragma once

#include "nixd/Protocol/AttrSet.h"

#include "lspserver/LSPServer.h"

#include <nix/eval.hh>

#include <memory>

namespace nixd {

/// \brief Main RPC class for attrset provider.
class AttrSetProvider : public lspserver::LSPServer {

std::unique_ptr<nix::EvalState> State;

nix::Value Nixpkgs;

/// Convenient method for get state. Basically assume this->State is not null
nix::EvalState &state() {
assert(State && "State should be allocated by ctor!");
return *State;
}

public:
AttrSetProvider(std::unique_ptr<lspserver::InboundPort> In,
std::unique_ptr<lspserver::OutboundPort> Out);

/// \brief Eval an expression, use it for furthur requests.
void onEvalExpr(const EvalExprParams &Name,
lspserver::Callback<EvalExprResponse> Reply);

/// \brief Query attrpath information.
void onAttrPathInfo(const AttrPathInfoParams &AttrPath,
lspserver::Callback<AttrPathInfoResponse> Reply);

/// \brief Complete attrpath entries.
void onAttrPathComplete(const AttrPathCompleteParams &Params,
lspserver::Callback<AttrPathCompleteResponse> Reply);
};

} // namespace nixd
47 changes: 47 additions & 0 deletions nixd/include/nixd/Protocol/AttrSet.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/// \file
/// \brief Types used in nixpkgs provider.

#pragma once

#include <optional>
#include <string>
#include <vector>

#include <llvm/Support/JSON.h>

namespace nixd {

using EvalExprParams = std::string;
using EvalExprResponse = std::optional<std::string>;

using AttrPathInfoParams = std::vector<std::string>;

struct PackageDescription {
std::optional<std::string> Name;
std::optional<std::string> PName;
std::optional<std::string> Version;
std::optional<std::string> Description;
std::optional<std::string> LongDescription;
std::optional<std::string> Position;
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);

struct AttrPathCompleteParams {
std::vector<std::string> Scope;
/// \brief Search for packages prefixed with this "prefix"
std::string Prefix;
};

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

using AttrPathCompleteResponse = std::vector<std::string>;

} // namespace nixd
133 changes: 133 additions & 0 deletions nixd/lib/Eval/AttrSetProvider.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#include "nixd/Eval/AttrSetProvider.h"

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

using namespace nixd;
using namespace lspserver;

namespace {

void fillString(nix::EvalState &State, nix::Value &V,
const std::vector<std::string_view> &AttrPath,
std::optional<std::string> &Field) {
try {
nix::Value &Select = nixt::selectStringViews(State, V, AttrPath);
State.forceValue(Select, nix::noPos);
if (Select.type() == nix::ValueType::nString)
Field = Select.string.c_str;
} catch (std::exception &E) {
Field = std::nullopt;
}
}

void fillPackageDescription(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);
}

} // namespace

AttrSetProvider::AttrSetProvider(std::unique_ptr<InboundPort> In,
std::unique_ptr<OutboundPort> Out)
: LSPServer(std::move(In), std::move(Out)),
State(new nix::EvalState({}, nix::openStore())) {
Registry.addMethod("attrset/evalExpr", this, &AttrSetProvider::onEvalExpr);
Registry.addMethod("attrset/attrpathInfo", this,
&AttrSetProvider::onAttrPathInfo);
Registry.addMethod("attrset/attrpathComplete", this,
&AttrSetProvider::onAttrPathComplete);
}

void AttrSetProvider::onEvalExpr(
const std::string &Name,
lspserver::Callback<std::optional<std::string>> Reply) {
try {
nix::Expr *AST = state().parseExprFromString(
Name, state().rootPath(nix::CanonPath::fromCwd()));
state().eval(AST, Nixpkgs);
Reply(std::nullopt);
return;
} catch (const nix::BaseError &Err) {
Reply(error(Err.info().msg.str()));
return;
} catch (const std::exception &Err) {
Reply(error(Err.what()));
return;
}
}

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);

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;
}
}

void AttrSetProvider::onAttrPathComplete(
const AttrPathCompleteParams &Params,
lspserver::Callback<AttrPathCompleteResponse> Reply) {
try {
nix::Value &Scope = nixt::selectStrings(state(), Nixpkgs, Params.Scope);

state().forceValue(Scope, nix::noPos);

if (Scope.type() != nix::ValueType::nAttrs) {
Reply(error("scope is not an attrset"));
return;
}

std::vector<std::string> Names;
int Num = 0;

// FIXME: we may want to use "Trie" to speedup the string searching.
// However as my (roughtly) profiling the critical in this loop is
// evaluating package details.
// "Trie"s may not beneficial becausae it cannot speedup eval.
for (const auto *AttrPtr :
Scope.attrs->lexicographicOrder(state().symbols)) {
const nix::Attr &Attr = *AttrPtr;
const std::string Name = state().symbols[Attr.name];
if (Name.starts_with(Params.Prefix)) {
++Num;
Names.emplace_back(Name);
// We set this a very limited number as to speedup
if (Num > 30)
break;
}
}
Reply(std::move(Names));
return;
} catch (const nix::BaseError &Err) {
Reply(error(Err.info().msg.str()));
return;
} catch (const std::exception &Err) {
Reply(error(Err.what()));
return;
}
}
42 changes: 42 additions & 0 deletions nixd/lib/Protocol/AttrSet.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#include "nixd/Protocol/AttrSet.h"

using namespace nixd;
using namespace llvm::json;

Value nixd::toJSON(const PackageDescription &Params) {
return Object{
{"Name", Params.Name},
{"PName", Params.PName},
{"Version", Params.Version},
{"Description", Params.Description},
{"LongDescription", Params.LongDescription},
{"Position", Params.Position},
{"Homepage", Params.Homepage},
};
}

bool nixd::fromJSON(const llvm::json::Value &Params, PackageDescription &R,
llvm::json::Path P) {
ObjectMapper O(Params, P);
return O //
&& O.map("Name", R.Name) //
&& O.map("PName", R.PName) //
&& O.map("Version", R.Version) //
&& O.map("Description", R.Description) //
&& O.map("LongDescription", R.LongDescription) //
&& O.map("Position", R.Position) //
&& O.map("Homepage", R.Homepage) //
;
}

Value nixd::toJSON(const AttrPathCompleteParams &Params) {
return Object{{"Scope", Params.Scope}, {"Prefix", Params.Prefix}};
}
bool nixd::fromJSON(const llvm::json::Value &Params, AttrPathCompleteParams &R,
llvm::json::Path P) {
ObjectMapper O(Params, P);
return O //
&& O.map("Scope", R.Scope) //
&& O.map("Prefix", R.Prefix) //
;
}
25 changes: 25 additions & 0 deletions nixd/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ libnixd_lib = library(
'lib/Controller/SemanticTokens.cpp',
'lib/Controller/Support.cpp',
'lib/Controller/TextDocumentSync.cpp',
'lib/Eval/AttrSetProvider.cpp',
'lib/Eval/EvalProvider.cpp',
'lib/Protocol/AttrSet.cpp',
'lib/Protocol/Protocol.cpp',
'lib/Support/AutoCloseFD.cpp',
'lib/Support/AutoRemoveShm.cpp',
Expand Down Expand Up @@ -68,3 +70,26 @@ nix_node_eval = executable(
dependencies: libnixd,
install_dir: get_option('libexecdir'),
)

nixd_attrset_eval = executable(
'nixd-attrset-eval',
'tools/nixd-attrset-eval.cpp',
install: true,
dependencies: libnixd,
install_dir: get_option('libexecdir'),
)

regression_env.set('ASAN_OPTIONS', 'detect_leaks=0')


if lit.found()
test(
'regression/nixd-attrset-eval',
lit,
env: regression_env,
args: [
'-vv',
meson.current_source_dir() + '/tools/nixd-attrset-eval/test'
],
depends: [ nixd_attrset_eval ] )
endif
Loading

0 comments on commit 7837528

Please sign in to comment.