nixd
Loading...
Searching...
No Matches
Completion.cpp
Go to the documentation of this file.
1/// \file
2/// \brief Implementation of [Code Completion].
3/// [Code Completion]:
4/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
5
6#include "AST.h"
7#include "CheckReturn.h"
8#include "Convert.h"
9
10#include "lspserver/Protocol.h"
11
14
16
17#include <boost/asio/post.hpp>
18
19#include <exception>
20#include <semaphore>
21#include <set>
22#include <utility>
23
24using namespace nixd;
25using namespace lspserver;
26using namespace nixf;
27
28namespace {
29
30/// Set max completion size to this value, we don't want to send large lists
31/// because of slow IO.
32/// Items exceed this size should be marked "incomplete" and recomputed.
33constexpr int MaxCompletionSize = 30;
34
37
38struct ExceedSizeError : std::exception {
39 [[nodiscard]] const char *what() const noexcept override {
40 return "Size exceeded";
41 }
42};
43
44void addItem(std::vector<CompletionItem> &Items, CompletionItem Item) {
45 if (Items.size() >= MaxCompletionSize) {
46 throw ExceedSizeError();
47 }
48 Items.emplace_back(std::move(Item));
49}
50
51bool hasConcreteChild(const Node &N) {
52 for (const Node *Child : N.children()) {
53 if (Child)
54 return true;
55 }
56 return false;
57}
58
59bool canCompleteAt(const Node &N) {
60 if (!hasConcreteChild(N))
61 return true;
62 return N.kind() == Node::NK_Binds || N.kind() == Node::NK_ExprAttrs;
63}
64
65bool isKnownSelector(const Node &N, const VariableLookupAnalysis &VLA,
66 const ParentMapAnalysis &PM) {
67 const Node *Expr = PM.upExpr(N);
68 if (!Expr || Expr->kind() != Node::NK_ExprSelect)
69 return false;
70
71 try {
72 idioms::mkSelector(static_cast<const nixf::ExprSelect &>(*Expr), VLA, PM);
73 return true;
74 } catch (const idioms::IdiomSelectorException &) {
75 return false;
76 } catch (const idioms::VLAException &) {
77 return false;
78 }
79}
80
81class VLACompletionProvider {
82 const VariableLookupAnalysis &VLA;
83
84 static CompletionItemKind getCompletionItemKind(const Definition &Def) {
85 if (Def.isBuiltin()) {
86 return CompletionItemKind::Keyword;
87 }
88 return CompletionItemKind::Variable;
89 }
90
91 /// Collect definition on some env, and also it's ancestors.
92 void collectDef(std::vector<CompletionItem> &Items, const EnvNode *Env,
93 const std::string &Prefix) {
94 if (!Env)
95 return;
96 collectDef(Items, Env->parent(), Prefix);
97 for (const auto &[Name, Def] : Env->defs()) {
98 if (Name.starts_with(
99 "__")) // These names are nix internal implementation, skip.
100 continue;
101 assert(Def);
102 if (Name.starts_with(Prefix)) {
103 addItem(Items, CompletionItem{
104 .label = Name,
105 .kind = getCompletionItemKind(*Def),
106 });
107 }
108 }
109 }
110
111public:
112 VLACompletionProvider(const VariableLookupAnalysis &VLA) : VLA(VLA) {}
113
114 /// Perform code completion right after this node.
115 void complete(const nixf::ExprVar &Desc, std::vector<CompletionItem> &Items,
116 const ParentMapAnalysis &PM) {
117 std::string Prefix = Desc.id().name();
118 collectDef(Items, upEnv(Desc, VLA, PM), Prefix);
119 }
120};
121
122/// \brief Provide completions by IPC. Asking nixpkgs provider.
123/// We simply select nixpkgs in separate process, thus this value does not need
124/// to be cached. (It is already cached in separate process.)
125///
126/// Currently, this procedure is explicitly blocked (synchronized). Because
127/// query nixpkgs value is relatively fast. In the future there might be nixd
128/// index, for performance.
129class NixpkgsCompletionProvider {
130
131 AttrSetClient &NixpkgsClient;
132
133public:
134 NixpkgsCompletionProvider(AttrSetClient &NixpkgsClient)
135 : NixpkgsClient(NixpkgsClient) {}
136
137 void resolvePackage(std::vector<std::string> Scope, std::string Name,
138 CompletionItem &Item) {
139 std::binary_semaphore Ready(0);
140 AttrPathInfoResponse Desc;
141 auto OnReply = [&Ready, &Desc](llvm::Expected<AttrPathInfoResponse> Resp) {
142 if (Resp)
143 Desc = *Resp;
144 Ready.release();
145 };
146 Scope.emplace_back(std::move(Name));
147 NixpkgsClient.attrpathInfo(Scope, std::move(OnReply));
148 Ready.acquire();
149 // Format "detail" and document.
150 const PackageDescription &PD = Desc.PackageDesc;
151 Item.documentation = MarkupContent{
152 .kind = MarkupKind::Markdown,
153 .value = PD.Description.value_or("") + "\n\n" +
154 PD.LongDescription.value_or(""),
155 };
156 Item.detail = PD.Version.value_or("?");
157 }
158
159 /// \brief Ask nixpkgs provider, give us a list of names. (thunks)
160 bool completePackages(const lspserver::Range EditRange,
161 const AttrPathCompleteParams &Params,
162 std::vector<CompletionItem> &Items) {
163 std::binary_semaphore Ready(0);
164 AttrPathCompleteResponse Names;
165 auto OnReply = [&Ready,
166 &Names](llvm::Expected<AttrPathCompleteResponse> Resp) {
167 if (!Resp) {
168 lspserver::elog("nixpkgs evaluator reported: {0}", Resp.takeError());
169 Ready.release();
170 return;
171 }
172 Names = *Resp; // Copy response to waiting thread.
173 Ready.release();
174 };
175 // Send request.
176 NixpkgsClient.attrpathComplete(Params, std::move(OnReply));
177 Ready.acquire();
178 // Now we have "Names", use these to fill "Items".
179 for (const auto &Name : Names.Items) {
180 if (Name.starts_with(Params.Prefix)) {
181 addItem(Items, CompletionItem{
182 .label = Name,
183 .kind = CompletionItemKind::Field,
184 .textEdit = lspserver::TextEdit{.range = EditRange,
185 .newText = Name},
186 .data = llvm::formatv("{0}", toJSON(Params)),
187 });
188 }
189 }
190 return Names.IsIncomplete;
191 }
192};
193
194/// \brief Provide completion list by nixpkgs module system (options).
195class OptionCompletionProvider {
196 AttrSetClient &OptionClient;
197
198 // Where is the module set. (e.g. nixos)
199 std::string ModuleOrigin;
200
201 // Wheter the client support code snippets.
202 bool ClientSupportSnippet;
203
204 static std::string escapeCharacters(const std::set<char> &Charset,
205 const std::string &Origin) {
206 // Escape characters listed in charset.
207 std::string Ret;
208 Ret.reserve(Origin.size());
209 for (const auto Ch : Origin) {
210 if (Charset.contains(Ch)) {
211 Ret += "\\";
212 Ret += Ch;
213 } else {
214 Ret += Ch;
215 }
216 }
217 return Ret;
218 }
219
220 void fillInsertText(CompletionItem &Item, const std::string &Name,
221 const std::string &Value) const {
222 if (!ClientSupportSnippet) {
223 Item.insertTextFormat = InsertTextFormat::PlainText;
224 Item.insertText = Name + " = " + Value + ";";
225 return;
226 }
227 Item.insertTextFormat = InsertTextFormat::Snippet;
228 Item.insertText = Name + " = " +
229 "${1:" + escapeCharacters({'\\', '$', '}'}, Value) + "}" +
230 ";";
231 }
232
233public:
234 OptionCompletionProvider(AttrSetClient &OptionClient,
235 std::string ModuleOrigin, bool ClientSupportSnippet)
236 : OptionClient(OptionClient), ModuleOrigin(std::move(ModuleOrigin)),
237 ClientSupportSnippet(ClientSupportSnippet) {}
238
239 bool completeOptions(const lspserver::Range EditRange,
240 std::vector<std::string> Scope, std::string Prefix,
241 std::vector<CompletionItem> &Items) {
242 std::binary_semaphore Ready(0);
243 OptionCompleteResponse Names;
244 auto OnReply = [&Ready,
245 &Names](llvm::Expected<OptionCompleteResponse> Resp) {
246 if (!Resp) {
247 lspserver::elog("option worker reported: {0}", Resp.takeError());
248 Ready.release();
249 return;
250 }
251 Names = *Resp; // Copy response to waiting thread.
252 Ready.release();
253 };
254 // Send request.
255 AttrPathCompleteParams Params{std::move(Scope), std::move(Prefix)};
256 OptionClient.optionComplete(Params, std::move(OnReply));
257 Ready.acquire();
258 // Now we have "Names", use these to fill "Items".
259 //
260 // When Params.Prefix is empty, the cursor is inside an empty hole and
261 // EditRange does not point at a real prefix to replace, so we omit the
262 // textEdit and let the editor fall back to insertText/label.
263 bool HasPrefix = !Params.Prefix.empty();
264 auto MkTextEdit =
265 [&](llvm::StringRef NewText) -> std::optional<lspserver::TextEdit> {
266 if (!HasPrefix)
267 return std::nullopt;
268 return lspserver::TextEdit{.range = EditRange, .newText = NewText.str()};
269 };
270 for (const nixd::OptionField &Field : Names.Items) {
271 if (!Field.Description) {
272 addItem(Items, CompletionItem{
273 .label = Field.Name,
274 .kind = OptionAttrKind,
275 .detail = ModuleOrigin,
276 .textEdit = MkTextEdit(Field.Name),
277 });
278 continue;
279 }
280
281 const OptionDescription &Desc = *Field.Description;
282
283 // Build the shared bits (detail, documentation) once, then emit one
284 // completion item per value source (`example`, `default`). This lets
285 // the user pick between the sample code the option author suggested
286 // and its default value - useful when an option only has one of the
287 // two, or when the user wants the default as a starting point.
288 std::string TypeDetail = ModuleOrigin + " | ";
289 if (Desc.Type) {
290 std::string TypeName = Desc.Type->Name.value_or("");
291 std::string TypeDesc = Desc.Type->Description.value_or("");
292 TypeDetail += llvm::formatv("{0} ({1})", TypeName, TypeDesc);
293 } else {
294 TypeDetail += "? (missing type)";
295 }
296 MarkupContent Doc{
297 .kind = MarkupKind::Markdown,
298 .value = Desc.Description.value_or(""),
299 };
300
301 // Check whether both variants will be emitted so we can decide
302 // whether to disambiguate the labels with "(example)" / "(default)".
303 bool HasExample = Desc.Example.has_value();
304 // Only emit a separate `default` item when it would differ from the
305 // example; otherwise the user would see two identical snippets.
306 bool HasDefault =
307 Desc.Default.has_value() && Desc.Default != Desc.Example;
308 bool HasBoth = HasExample && HasDefault;
309
310 auto emit = [&](const std::string &Value, llvm::StringRef Source,
311 llvm::StringRef SortPrefix) {
312 // When both variants exist, append the source so users can tell
313 // them apart. `filterText` is always the plain option name so
314 // typing the name matches both items.
315 std::string Label =
316 HasBoth ? llvm::formatv("{0} ({1})", Field.Name, Source).str()
317 : Field.Name;
318 CompletionItem Item{
319 .label = std::move(Label),
320 .kind = OptionKind,
321 .detail = TypeDetail,
322 .documentation = Doc,
323 .sortText = (SortPrefix + Field.Name).str(),
324 .filterText = Field.Name,
325 };
326 fillInsertText(Item, Field.Name, Value);
327 Item.textEdit = MkTextEdit(Item.insertText);
328 addItem(Items, std::move(Item));
329 };
330
331 bool Emitted = false;
332 if (HasExample) {
333 emit(*Desc.Example, "example", "0");
334 Emitted = true;
335 }
336 if (HasDefault) {
337 emit(*Desc.Default, "default", "1");
338 Emitted = true;
339 }
340 if (!Emitted) {
341 // No example or default to insert — still offer the option name
342 // as a bare completion so users can discover it.
343 CompletionItem Item{
344 .label = Field.Name,
345 .kind = OptionKind,
346 .detail = TypeDetail,
347 .documentation = Doc,
348 };
349 fillInsertText(Item, Field.Name, "");
350 Item.textEdit = MkTextEdit(Item.insertText);
351 addItem(Items, std::move(Item));
352 }
353 }
354 return Names.IsIncomplete;
355 }
356};
357
358bool completeAttrName(const lspserver::Range EditRange,
359 const std::vector<std::string> &Scope,
360 const std::string &Prefix,
361 Controller::OptionMapTy &Options, bool CompletionSnippets,
362 std::vector<CompletionItem> &List) {
363 bool IsIncomplete = false;
364 for (const auto &[Name, Provider] : Options) {
365 AttrSetClient *Client = Options.at(Name)->client();
366 if (!Client) [[unlikely]] {
367 elog("skipped client {0} as it is dead", Name);
368 continue;
369 }
370 OptionCompletionProvider OCP(*Client, Name, CompletionSnippets);
371 IsIncomplete |= OCP.completeOptions(EditRange, Scope, Prefix, List);
372 }
373 return IsIncomplete;
374}
375
376bool completeAttrPath(const lspserver::Range EditRange, const Node &N,
377 const ParentMapAnalysis &PM, std::mutex &OptionsLock,
378 Controller::OptionMapTy &Options, bool Snippets,
379 std::vector<lspserver::CompletionItem> &Items) {
380 std::vector<std::string> Scope;
381 using PathResult = FindAttrPathResult;
382 auto R = findAttrPathForOptions(N, PM, Scope);
383 if (R == PathResult::OK) {
384 // Construct request.
385 std::string Prefix = Scope.back();
386 Scope.pop_back();
387 {
388 std::lock_guard _(OptionsLock);
389 return completeAttrName(EditRange, Scope, Prefix, Options, Snippets,
390 Items);
391 }
392 }
393 return false;
394}
395
396AttrPathCompleteParams mkParams(nixd::Selector Sel, bool IsComplete) {
397 if (IsComplete || Sel.empty()) {
398 return {
399 .Scope = std::move(Sel),
400 .Prefix = "",
401 };
402 }
403 std::string Back = std::move(Sel.back());
404 Sel.pop_back();
405 return {
406 .Scope = Sel,
407 .Prefix = std::move(Back),
408 };
409}
410
411#define DBG DBGPREFIX ": "
412
413bool completeVarName(const lspserver::Range EditRange,
414 const VariableLookupAnalysis &VLA,
415 const ParentMapAnalysis &PM, const nixf::ExprVar &N,
416 AttrSetClient &Client, std::vector<CompletionItem> &List) {
417#define DBGPREFIX "completion/var"
418
419 VLACompletionProvider VLAP(VLA);
420 VLAP.complete(N, List, PM);
421
422 // Try to complete the name by known idioms.
423 try {
424 Selector Sel = idioms::mkVarSelector(N, VLA, PM);
425
426 // Clickling "pkgs" does not make sense for variable completion
427 if (Sel.empty())
428 return false;
429
430 // Invoke nixpkgs provider to get the completion list.
431 NixpkgsCompletionProvider NCP(Client);
432 // Variable names are partial selector segments.
433 return NCP.completePackages(EditRange, mkParams(Sel, /*IsComplete=*/false),
434 List);
435 } catch (ExceedSizeError &) {
436 // Let "onCompletion" catch this exception to set "inComplete" field.
437 throw;
438 } catch (std::exception &E) {
439 log(DBG "skipped, reason: {0}", E.what());
440 return false;
441 }
442
443#undef DBGPREFIX
444}
445
446/// \brief Complete a "select" expression.
447/// \param IsComplete Whether or not the last element of the selector is
448/// effectively incomplete.
449/// e.g.
450/// - incomplete: `lib.gen|`
451/// - complete: `lib.attrset.|`
452bool completeSelect(const lspserver::Range EditRange,
453 const nixf::ExprSelect &Select, AttrSetClient &Client,
455 const nixf::ParentMapAnalysis &PM, bool IsComplete,
456 std::vector<CompletionItem> &List) {
457#define DBGPREFIX "completion/select"
458 // The base expr for selecting.
459 const nixf::Expr &BaseExpr = Select.expr();
460
461 // Determine that the name is one of special names interesting
462 // for nix language. If it is not a simple variable, skip this
463 // case.
464 if (BaseExpr.kind() != Node::NK_ExprVar) {
465 return false;
466 }
467
468 const auto &Var = static_cast<const nixf::ExprVar &>(BaseExpr);
469 // Ask nixpkgs provider to get idioms completion.
470 NixpkgsCompletionProvider NCP(Client);
471
472 try {
473 Selector Sel =
474 idioms::mkSelector(Select, idioms::mkVarSelector(Var, VLA, PM));
475 return NCP.completePackages(EditRange, mkParams(Sel, IsComplete), List);
476 } catch (ExceedSizeError &) {
477 // Let "onCompletion" catch this exception to set "inComplete" field.
478 throw;
479 } catch (std::exception &E) {
480 log(DBG "skipped, reason: {0}", E.what());
481 return false;
482 }
483
484#undef DBGPREFIX
485}
486
487} // namespace
488
489void Controller::onCompletion(const CompletionParams &Params,
491 using CheckTy = CompletionList;
492 auto Action = [Reply = std::move(Reply), URI = Params.textDocument.uri,
493 Pos = toNixfPosition(Params.position),
494 Context = Params.context, this]() mutable {
495 const auto File = URI.file().str();
496 bool KnownSelector = false;
497 auto Result = [&]() -> llvm::Expected<CompletionList> {
498 const auto TU = CheckDefault(getTU(File));
499 const auto AST = CheckDefault(getAST(*TU));
500 const auto &PM = *TU->parentMap();
501 const VariableLookupAnalysis &VLA = *TU->variableLookup();
502
503 const auto *Desc = AST->descend({Pos, Pos});
504 if (Desc)
505 KnownSelector = isKnownSelector(*Desc, VLA, PM);
506 CheckDefault(Desc && canCompleteAt(*Desc));
507
508 const auto &N = *Desc;
509 const auto &UpExpr = *CheckDefault(PM.upExpr(N));
510
511 lspserver::Range EditRange = toLSPRange(TU->src(), N.range());
512 if (N.kind() == Node::NK_Dot) {
513 // If the node is a dot, insert after the dot
514 EditRange.start = EditRange.end;
515 }
516
517 CompletionList List;
518 bool ProviderIncomplete = false;
519 try {
520 switch (UpExpr.kind()) {
521 // In these cases, assume the cursor have "variable" scoping.
522 case Node::NK_ExprVar: {
523 ProviderIncomplete = completeVarName(
524 EditRange, VLA, PM, static_cast<const nixf::ExprVar &>(UpExpr),
525 *nixpkgsClient(), List.items);
526 break;
527 }
528 // A "select" expression. e.g.
529 // foo.a|
530 // foo.|
531 // foo.a.bar|
532 case Node::NK_ExprSelect: {
533 const auto &Select = static_cast<const nixf::ExprSelect &>(UpExpr);
534 ProviderIncomplete =
535 completeSelect(EditRange, Select, *nixpkgsClient(), VLA, PM,
536 N.kind() == Node::NK_Dot, List.items);
537 break;
538 }
539 case Node::NK_ExprAttrs: {
540 ProviderIncomplete =
541 completeAttrPath(EditRange, N, PM, OptionsLock, Options,
542 ClientCaps.CompletionSnippets, List.items);
543 break;
544 }
545 default:
546 break;
547 }
548 } catch (ExceedSizeError &Err) {
549 List.isIncomplete = true;
550 return List;
551 }
552 List.isIncomplete |= ProviderIncomplete;
553 return List;
554 }();
555
556 // A recognized selector may have no candidates for the empty prefix. Keep
557 // that trigger-character result retriggerable so a more specific prefix
558 // can be requested as the user continues typing.
559 if (Result &&
560 Context.triggerKind == CompletionTriggerKind::TriggerCharacter &&
561 Context.triggerCharacter == "." && KnownSelector &&
562 Result->items.empty())
563 Result->isIncomplete = true;
564 return Reply(std::move(Result));
565 };
566 boost::asio::post(Pool, std::move(Action));
567}
568
569void Controller::onCompletionItemResolve(const CompletionItem &Params,
571
572 auto Action = [Params, Reply = std::move(Reply), this]() mutable {
573 if (Params.data.empty()) {
574 Reply(Params);
575 return;
576 }
577 AttrPathCompleteParams Req;
578 auto EV = llvm::json::parse(Params.data);
579 if (!EV) {
580 // If the json value cannot be parsed, this is very unlikely to happen.
581 Reply(EV.takeError());
582 return;
583 }
584
585 llvm::json::Path::Root Root;
586 fromJSON(*EV, Req, Root);
587
588 // FIXME: handle null nixpkgsClient()
589 NixpkgsCompletionProvider NCP(*nixpkgsClient());
590 CompletionItem Resp = Params;
591 NCP.resolvePackage(Req.Scope, Params.label, Resp);
592
593 Reply(std::move(Resp));
594 };
595 boost::asio::post(Pool, std::move(Action));
596}
This file declares some common analysis (tree walk) on the AST.
Types used in nixpkgs provider.
#define CheckDefault(x)
Variant of CheckReturn, but returns default constructed CheckTy.
Definition CheckReturn.h:16
#define DBG
Convert between LSP and nixf types.
Lookup variable names, from it's parent scope.
std::map< std::string, std::unique_ptr< AttrSetClientProc > > OptionMapTy
Definition Controller.h:21
bool isBuiltin() const
EnvNode * parent() const
const DefMap & defs() const
Expr & expr() const
Definition Expr.h:24
const Identifier & id() const
Definition Simple.h:200
const std::string & name() const
Definition Basic.h:120
NodeKind kind() const
Definition Basic.h:34
LexerCursorRange range() const
Definition Basic.h:35
virtual ChildVector children() const =0
const Node * upExpr(const Node &N) const
Search up until the node becomes a concrete expression. a ^<--— ID -> ExprVar.
Definition ParentMap.cpp:17
Whether current platform treats paths case insensitively.
Definition Connection.h:11
llvm::unique_function< void(llvm::Expected< T >)> Callback
Definition Function.h:14
void elog(const char *Fmt, Ts &&...Vals)
Definition Logger.h:52
CompletionItemKind
The kind of a completion entry.
void log(const char *Fmt, Ts &&...Vals)
Definition Logger.h:58
Selector mkVarSelector(const nixf::ExprVar &Var, const nixf::VariableLookupAnalysis &VLA, const nixf::ParentMapAnalysis &PM)
Construct a nixd::Selector from Var.
Definition AST.cpp:199
Selector mkSelector(const nixf::AttrPath &AP, Selector BaseSelector)
Construct a nixd::Selector from AP.
Definition AST.cpp:249
bool fromJSON(const llvm::json::Value &Params, Configuration::Diagnostic &R, llvm::json::Path P)
llvm::json::Value toJSON(const PackageDescription &Params)
Definition AttrSet.cpp:73
const nixf::EnvNode * upEnv(const nixf::Node &Desc, const nixf::VariableLookupAnalysis &VLA, const nixf::ParentMapAnalysis &PM)
Search up until there are some node associated with "EnvNode".
Definition AST.cpp:98
std::vector< std::string > Selector
A list of strings that "select"s into a attribute set.
Definition AttrSet.h:43
FindAttrPathResult
Definition AST.h:119
nixf::Position toNixfPosition(const lspserver::Position &P)
Definition Convert.cpp:32
FindAttrPathResult findAttrPathForOptions(const nixf::Node &N, const nixf::ParentMapAnalysis &PM, std::vector< std::string > &Path)
Heuristically find attrpath suitable for "attrpath" completion. Strips "config." from the start to su...
Definition AST.cpp:332
lspserver::Range toLSPRange(llvm::StringRef Code, const nixf::LexerCursorRange &R)
Definition Convert.cpp:40
std::optional< MarkupContent > documentation
A human-readable string that represents a doc-comment.
std::vector< CompletionItem > items
The completion items.
Position start
The range's start position.
Position end
The range's end position.
Position position
The position inside the text document.
TextDocumentIdentifier textDocument
The text document.
std::string Prefix
Search for packages prefixed with this "prefix".
Definition AttrSet.h:110
std::vector< std::string > Items
Definition AttrSet.h:118
PackageDescription PackageDesc
Package description of the attribute path, if available.
Definition AttrSet.h:98
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::optional< std::string > Default
Definition AttrSet.h:140
std::optional< std::string > Version
Definition AttrSet.h:53
std::optional< std::string > Description
Definition AttrSet.h:54
std::optional< std::string > LongDescription
Definition AttrSet.h:55
Exceptions scoped in nixd::mkIdiomSelector.
Definition AST.h:31