nixd
Loading...
Searching...
No Matches
AttrSetProvider.cpp
Go to the documentation of this file.
3
5
6#include <nix/cmd/common-eval-args.hh>
7#include <nix/expr/attr-path.hh>
8#include <nix/expr/nixexpr.hh>
9#include <nix/store/store-open.hh>
10#include <nixt/Value.h>
11
12using namespace nixd;
13using namespace lspserver;
14
15namespace {
16
17constexpr int MaxItems = 30;
18
19void fillString(nix::EvalState &State, nix::Value &V,
20 const std::vector<std::string_view> &AttrPath,
21 std::optional<std::string> &Field) {
22 try {
23 nix::Value &Select = nixt::selectStringViews(State, V, AttrPath);
24 State.forceValue(Select, nix::noPos);
25 if (Select.type() == nix::ValueType::nString)
26 Field = Select.string_view();
27 } catch (std::exception &E) {
28 Field = std::nullopt;
29 }
30}
31
32/// Describe the value as if \p Package is actually a nixpkgs package.
33PackageDescription describePackage(nix::EvalState &State, nix::Value &Package) {
35 fillString(State, Package, {"name"}, R.Name);
36 fillString(State, Package, {"pname"}, R.PName);
37 fillString(State, Package, {"version"}, R.Version);
38 fillString(State, Package, {"meta", "description"}, R.Description);
39 fillString(State, Package, {"meta", "longDescription"}, R.LongDescription);
40 fillString(State, Package, {"meta", "position"}, R.Position);
41 fillString(State, Package, {"meta", "homepage"}, R.Homepage);
42 return R;
43}
44
45std::optional<Location> locationOf(nix::PosTable &PTable, nix::Value &V) {
46 nix::PosIdx P = V.determinePos(nix::noPos);
47 if (!P)
48 return std::nullopt;
49
50 nix::Pos NixPos = PTable[P];
51 const auto *SP = std::get_if<nix::SourcePath>(&NixPos.origin);
52
53 if (!SP)
54 return std::nullopt;
55
56 Position LPos = {
57 .line = static_cast<int64_t>(NixPos.line - 1),
58 .character = static_cast<int64_t>(NixPos.column - 1),
59 };
60
61 return Location{
62 .uri = URIForFile::canonicalize(SP->path.abs(), SP->path.abs()),
63 .range = {LPos, LPos},
64 };
65}
66
67ValueMeta metadataOf(nix::EvalState &State, nix::Value &V) {
68 return {
69 .Type = V.type<true>(),
70 .Location = locationOf(State.positions, V),
71 };
72}
73
74void fillUnsafeGetAttrPosLocation(nix::EvalState &State, nix::Value &V,
76 State.forceValue(V, nix::noPos);
77 nix::Value &File = nixt::selectAttr(State, V, State.symbols.create("file"));
78 nix::Value &Line = nixt::selectAttr(State, V, State.symbols.create("line"));
79 nix::Value &Column =
80 nixt::selectAttr(State, V, State.symbols.create("column"));
81
82 State.forceValue(File, nix::noPos);
83 State.forceValue(Line, nix::noPos);
84 State.forceValue(Column, nix::noPos);
85
86 if (File.type() == nix::ValueType::nString)
87 Loc.uri = URIForFile::canonicalize(File.c_str(), File.c_str());
88
89 if (Line.type() == nix::ValueType::nInt &&
90 Column.type() == nix::ValueType::nInt) {
91
92 // Nix position starts from "1" however lsp starts from zero.
93 lspserver::Position Pos = {static_cast<int64_t>(Line.integer()) - 1,
94 static_cast<int64_t>(Column.integer()) - 1};
95 Loc.range = {Pos, Pos};
96 }
97}
98
99void fillOptionDeclarationPositions(nix::EvalState &State, nix::Value &V,
101 State.forceValue(V, nix::noPos);
102 if (V.type() != nix::ValueType::nList)
103 return;
104 for (nix::Value *Item : V.listView()) {
105 // Each item should have "column", "line", "file" fields.
107 fillUnsafeGetAttrPosLocation(State, *Item, Loc);
108 R.Declarations.emplace_back(std::move(Loc));
109 }
110}
111
112void fillOptionDeclarations(nix::EvalState &State, nix::Value &V,
114 // Eval declarations
115 try {
116 nix::Value &DeclarationPositions = nixt::selectAttr(
117 State, V, State.symbols.create("declarationPositions"));
118
119 State.forceValue(DeclarationPositions, nix::noPos);
120 // A list of positions, in unsafeGetAttrPos format.
121 fillOptionDeclarationPositions(State, DeclarationPositions, R);
122 } catch (nix::AttrPathNotFound &E) {
123 // FIXME: fallback to "declarations"
124 return;
125 }
126}
127
128void fillOptionType(nix::EvalState &State, nix::Value &VType, OptionType &R) {
129 fillString(State, VType, {"description"}, R.Description);
130 fillString(State, VType, {"name"}, R.Name);
131}
132
133/// Render an option's `example` or `default` field as a string suitable for
134/// code completion. Handles `literalExpression` wrappers as used in nixpkgs.
135/// When \p AllowComplex is false, only scalar values (strings, numbers,
136/// booleans, null, paths) are rendered; attrsets, lists, lambdas, and thunks
137/// are skipped to avoid infinite recursion.
138std::optional<std::string> renderOptionValue(nix::EvalState &State,
139 nix::Value &V, bool AllowComplex) {
140 try {
141 State.forceValue(V, nix::noPos);
142
143 // In nixpkgs these fields are often wrapped in `literalExpression`
144 // which carries the source text.
145 if (nixt::checkField(State, V, "_type", "literalExpression")) {
146 if (auto Text = nixt::getFieldString(State, V, "text"))
147 return std::string(*Text);
148 return std::nullopt;
149 }
150
151 if (!AllowComplex) {
152 switch (V.type()) {
153 case nix::ValueType::nString:
154 case nix::ValueType::nInt:
155 case nix::ValueType::nFloat:
156 case nix::ValueType::nBool:
157 case nix::ValueType::nNull:
158 case nix::ValueType::nPath:
159 break;
160 default:
161 return std::nullopt;
162 }
163 }
164
165 std::ostringstream OS;
166 V.print(State, OS);
167 return OS.str();
168 } catch (std::exception &) {
169 return std::nullopt;
170 }
171}
172
173void fillOptionDescription(nix::EvalState &State, nix::Value &V,
175 fillString(State, V, {"description"}, R.Description);
176 fillOptionDeclarations(State, V, R);
177 // FIXME: add definitions location.
178 if (V.type() == nix::ValueType::nAttrs) [[likely]] {
179 assert(V.attrs());
180 if (auto *It = V.attrs()->get(State.symbols.create("type"))) [[likely]] {
182 fillOptionType(State, *It->value, Type);
183 R.Type = std::move(Type);
184 }
185
186 if (auto *It = V.attrs()->get(State.symbols.create("example"))) {
187 R.Example = renderOptionValue(State, *It->value, /*AllowComplex=*/true);
188 }
189
190 // Fall back to the option's default so completion still has something
191 // useful to offer when no `example` is provided. `defaultText` takes
192 // priority over `default`: nixpkgs authors set `defaultText` precisely
193 // when the raw `default` would be unhelpful (e.g. a self-referential
194 // attrset or lambda), so its presence alone means `default` should be
195 // ignored — falling back would defeat the author's intent. Complex
196 // raw defaults are also refused to avoid infinite recursion.
197 if (auto *It = V.attrs()->get(State.symbols.create("defaultText"))) {
198 R.Default = renderOptionValue(State, *It->value, /*AllowComplex=*/false);
199 } else if (auto *It = V.attrs()->get(State.symbols.create("default"))) {
200 R.Default = renderOptionValue(State, *It->value, /*AllowComplex=*/false);
201 }
202 }
203}
204
205AttrPathCompleteResponse completeNames(nix::Value &Scope,
206 const nix::EvalState &State,
207 std::string_view Prefix) {
209
210 // FIXME: we may want to use "Trie" to speedup the string searching.
211 // However as my (roughtly) profiling the critical in this loop is
212 // evaluating package details.
213 // "Trie"s may not beneficial because it cannot speedup eval.
214 for (const auto *AttrPtr : Scope.attrs()->lexicographicOrder(State.symbols)) {
215 const nix::Attr &Attr = *AttrPtr;
216 const std::string_view Name = State.symbols[Attr.name];
217 if (Name.starts_with(Prefix)) {
218 if (Result.Items.size() >= MaxItems) {
219 Result.IsIncomplete = true;
220 break;
221 }
222 Result.Items.emplace_back(Name);
223 }
224 }
225 return Result;
226}
227
228std::optional<ValueDescription> describeValue(nix::EvalState &State,
229 nix::Value &V) {
230 if (V.isPrimOp()) {
231 const auto *PrimOp = V.primOp();
232 assert(PrimOp);
233 return ValueDescription{
234 .Doc = PrimOp->doc.value_or(""),
235 .Arity = static_cast<int>(PrimOp->arity),
236 .Args = PrimOp->args,
237 };
238 } else if (V.isLambda()) {
239 auto *Lambda = V.lambda().fun;
240 assert(Lambda);
241 const auto DocComment = Lambda->docComment;
242
243 // We can only get the comment in the function, not the Arity and Args
244 // information. Therefore, if the comment doesn't exist, return
245 // `std::nullopt`, indicating that we didn't get any valuable information.
246 // https://github.com/NixOS/nix/blob/ee59af99f8619e17db4289843da62a24302d20b7/src/libexpr/eval.cc#L638
247 if (!DocComment)
248 return std::nullopt;
249
250 return ValueDescription{
251 .Doc = DocComment.getInnerText(State.positions),
252 .Arity = 0,
253 .Args = {},
254 };
255 }
256
257 return std::nullopt;
258}
259
260} // namespace
261
262AttrSetProvider::AttrSetProvider(std::unique_ptr<InboundPort> In,
263 std::unique_ptr<OutboundPort> Out)
264 : LSPServer(std::move(In), std::move(Out)),
265 State(new nix::EvalState({}, nix::openStore(), nix::fetchSettings,
266 nix::evalSettings)) {
267 Registry.addMethod(rpcMethod::EvalExpr, this, &AttrSetProvider::onEvalExpr);
268 Registry.addMethod(rpcMethod::AttrPathInfo, this,
270 Registry.addMethod(rpcMethod::AttrPathComplete, this,
272 Registry.addMethod(rpcMethod::OptionInfo, this,
274 Registry.addMethod(rpcMethod::OptionComplete, this,
276}
277
279 const std::string &Name,
280 lspserver::Callback<std::optional<std::string>> Reply) {
281 try {
282 nix::Expr *AST = state().parseExprFromString(Name, state().rootPath("."));
283 state().eval(AST, Nixpkgs);
284 Reply(std::nullopt);
285 return;
286 } catch (const nix::BaseError &Err) {
287 Reply(error(Err.info().msg.str()));
288 return;
289 } catch (const std::exception &Err) {
290 Reply(error(Err.what()));
291 return;
292 }
293}
294
296 const AttrPathInfoParams &AttrPath,
298 using RespT = AttrPathInfoResponse;
299 Reply([&]() -> llvm::Expected<RespT> {
300 try {
301 if (AttrPath.empty())
302 return error("attrpath is empty!");
303
304 nix::Value &V = nixt::selectStrings(state(), Nixpkgs, AttrPath);
305 state().forceValue(V, nix::noPos);
306 return RespT{
307 .Meta = metadataOf(state(), V),
308 .PackageDesc = describePackage(state(), V),
309 .ValueDesc = describeValue(state(), V),
310 };
311 } catch (const nix::BaseError &Err) {
312 return error(Err.info().msg.str());
313 } catch (const std::exception &Err) {
314 return error(Err.what());
315 }
316 }());
317}
318
320 const AttrPathCompleteParams &Params,
322 try {
323 nix::Value &Scope = nixt::selectStrings(state(), Nixpkgs, Params.Scope);
324
325 state().forceValue(Scope, nix::noPos);
326
327 if (Scope.type() != nix::ValueType::nAttrs) {
328 Reply(error("scope is not an attrset"));
329 return;
330 }
331
332 return Reply(completeNames(Scope, state(), Params.Prefix));
333 } catch (const nix::BaseError &Err) {
334 return Reply(error(Err.info().msg.str()));
335 } catch (const std::exception &Err) {
336 return Reply(error(Err.what()));
337 }
338}
339
341 const AttrPathInfoParams &AttrPath,
343 try {
344 if (AttrPath.empty()) {
345 Reply(error("attrpath is empty!"));
346 return;
347 }
348
349 nix::Value Option = nixt::selectOptions(
350 state(), Nixpkgs, nixt::toSymbols(state().symbols, AttrPath));
351
353
354 fillOptionDescription(state(), Option, R);
355
356 Reply(std::move(R));
357 return;
358 } catch (const nix::BaseError &Err) {
359 Reply(error(Err.info().msg.str()));
360 return;
361 } catch (const std::exception &Err) {
362 Reply(error(Err.what()));
363 return;
364 }
365}
366
368 const AttrPathCompleteParams &Params,
370 try {
371 nix::Value Scope = nixt::selectOptions(
372 state(), Nixpkgs, nixt::toSymbols(state().symbols, Params.Scope));
373
374 state().forceValue(Scope, nix::noPos);
375
376 if (Scope.type() != nix::ValueType::nAttrs) {
377 Reply(error("scope is not an attrset"));
378 return;
379 }
380
381 if (nixt::isOption(state(), Scope)) {
382 Reply(error("scope is already an option"));
383 return;
384 }
385
386 OptionCompleteResponse Response;
387
388 // FIXME: we may want to use "Trie" to speedup the string searching.
389 // However as my (roughtly) profiling the critical in this loop is
390 // evaluating package details.
391 // "Trie"s may not beneficial becausae it cannot speedup eval.
392 for (const auto *AttrPtr :
393 Scope.attrs()->lexicographicOrder(state().symbols)) {
394 const nix::Attr &Attr = *AttrPtr;
395 std::string_view Name = state().symbols[Attr.name];
396 if (Name.starts_with(Params.Prefix)) {
397 if (Response.Items.size() >= MaxItems) {
398 Response.IsIncomplete = true;
399 break;
400 }
401
402 // Add a new "OptionField", see it's type.
403 assert(Attr.value);
404 OptionField NewField;
405 NewField.Name = Name;
406 if (nixt::isOption(state(), *Attr.value)) {
408 fillOptionDescription(state(), *Attr.value, Desc);
409 NewField.Description = std::move(Desc);
410 }
411 Response.Items.emplace_back(std::move(NewField));
412 }
413 }
414 Reply(std::move(Response));
415 return;
416 } catch (const nix::BaseError &Err) {
417 Reply(error(Err.info().msg.str()));
418 return;
419 } catch (const std::exception &Err) {
420 Reply(error(Err.what()));
421 return;
422 }
423}
Dedicated worker for evaluating attrset.
Types used in nixpkgs provider.
LSPServer(std::unique_ptr< InboundPort > In, std::unique_ptr< OutboundPort > Out)
Definition LSPServer.h:87
void onOptionInfo(const AttrPathInfoParams &AttrPath, lspserver::Callback< OptionInfoResponse > Reply)
Provide option information on given attrpath.
void onAttrPathInfo(const AttrPathInfoParams &AttrPath, lspserver::Callback< AttrPathInfoResponse > Reply)
Query attrpath information.
void onEvalExpr(const EvalExprParams &Name, lspserver::Callback< EvalExprResponse > Reply)
Eval an expression, use it for furthur requests.
AttrSetProvider(std::unique_ptr< lspserver::InboundPort > In, std::unique_ptr< lspserver::OutboundPort > Out)
void onAttrPathComplete(const AttrPathCompleteParams &Params, lspserver::Callback< AttrPathCompleteResponse > Reply)
Complete attrpath entries.
void onOptionComplete(const AttrPathCompleteParams &Params, lspserver::Callback< OptionCompleteResponse > Reply)
Complete attrpath entries. However dive into submodules while selecting.
Whether current platform treats paths case insensitively.
Definition Connection.h:11
llvm::unique_function< void(llvm::Expected< T >)> Callback
Definition Function.h:14
llvm::Error error(std::error_code EC, const char *Fmt, Ts &&...Vals)
Definition Logger.h:70
constexpr std::string_view EvalExpr
Definition AttrSet.h:30
constexpr std::string_view OptionInfo
Definition AttrSet.h:33
constexpr std::string_view AttrPathInfo
Definition AttrSet.h:31
constexpr std::string_view OptionComplete
Definition AttrSet.h:34
constexpr std::string_view AttrPathComplete
Definition AttrSet.h:32
OptionDescription OptionInfoResponse
Definition AttrSet.h:157
Selector AttrPathInfoParams
Definition AttrSet.h:48
std::optional< std::string_view > getFieldString(nix::EvalState &State, nix::Value &V, std::string_view Field)
Definition Value.cpp:23
bool isOption(nix::EvalState &State, nix::Value &V)
Definition Value.cpp:46
bool checkField(nix::EvalState &State, nix::Value &V, std::string_view Field, std::string_view Pred)
Check if value V is an attrset, has the field, and equals to Pred.
Definition Value.cpp:36
nix::Value & selectStrings(nix::EvalState &State, nix::Value &V, const std::vector< std::string > &AttrPath)
Given an attrpath in nix::Value V, select it.
Definition Value.h:61
nix::Value selectOptions(nix::EvalState &State, nix::Value &V, std::vector< nix::Symbol >::const_iterator Begin, std::vector< nix::Symbol >::const_iterator End)
Select the option declaration list, V, dive into "submodules".
Definition Value.cpp:159
std::vector< nix::Symbol > toSymbols(nix::SymbolTable &STable, const std::vector< std::string > &Names)
Transform a vector of string into a vector of nix symbols.
Definition Value.cpp:63
nix::Value & selectAttr(nix::EvalState &State, nix::Value &V, nix::Symbol Attr)
Select attribute Attr.
Definition Value.cpp:84
nix::Value & selectStringViews(nix::EvalState &State, nix::Value &V, const std::vector< std::string_view > &AttrPath)
Given an attrpath in nix::Value V, select it.
Definition Value.h:68
URIForFile uri
The text document's URI.
static URIForFile canonicalize(llvm::StringRef AbsPath, llvm::StringRef TUPath)
std::string Prefix
Search for packages prefixed with this "prefix".
Definition AttrSet.h:110
std::vector< std::string > Items
Definition AttrSet.h:118
std::vector< OptionField > Items
Definition AttrSet.h:160
std::optional< std::string > Description
Definition AttrSet.h:136
std::optional< std::string > Example
Definition AttrSet.h:139
std::optional< OptionType > Type
Definition AttrSet.h:141
std::vector< lspserver::Location > Declarations
Definition AttrSet.h:137
std::optional< std::string > Default
Definition AttrSet.h:140
std::optional< OptionDescription > Description
Definition AttrSet.h:150
std::string Name
Definition AttrSet.h:149
std::optional< std::string > Description
Definition AttrSet.h:127
std::optional< std::string > Name
Definition AttrSet.h:128
std::optional< std::string > Name
Definition AttrSet.h:51
std::optional< std::string > Version
Definition AttrSet.h:53
std::optional< std::string > PName
Definition AttrSet.h:52
std::optional< std::string > Description
Definition AttrSet.h:54
std::optional< std::string > LongDescription
Definition AttrSet.h:55
std::optional< std::string > Position
Definition AttrSet.h:56
std::optional< std::string > Homepage
Definition AttrSet.h:57
Using nix's ":doc" method to retrieve value's additional information.
Definition AttrSet.h:83
General metadata of all nix::Values.
Definition AttrSet.h:65