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
205std::vector<std::string> completeNames(nix::Value &Scope,
206 const nix::EvalState &State,
207 std::string_view Prefix) {
208 int Num = 0;
209 std::vector<std::string> Names;
210
211 // FIXME: we may want to use "Trie" to speedup the string searching.
212 // However as my (roughtly) profiling the critical in this loop is
213 // evaluating package details.
214 // "Trie"s may not beneficial because it cannot speedup eval.
215 for (const auto *AttrPtr : Scope.attrs()->lexicographicOrder(State.symbols)) {
216 const nix::Attr &Attr = *AttrPtr;
217 const std::string_view Name = State.symbols[Attr.name];
218 if (Name.starts_with(Prefix)) {
219 ++Num;
220 Names.emplace_back(Name);
221 // We set this a very limited number as to speedup
222 if (Num > MaxItems)
223 break;
224 }
225 }
226 return Names;
227}
228
229std::optional<ValueDescription> describeValue(nix::EvalState &State,
230 nix::Value &V) {
231 if (V.isPrimOp()) {
232 const auto *PrimOp = V.primOp();
233 assert(PrimOp);
234 return ValueDescription{
235 .Doc = PrimOp->doc.value_or(""),
236 .Arity = static_cast<int>(PrimOp->arity),
237 .Args = PrimOp->args,
238 };
239 } else if (V.isLambda()) {
240 auto *Lambda = V.lambda().fun;
241 assert(Lambda);
242 const auto DocComment = Lambda->docComment;
243
244 // We can only get the comment in the function, not the Arity and Args
245 // information. Therefore, if the comment doesn't exist, return
246 // `std::nullopt`, indicating that we didn't get any valuable information.
247 // https://github.com/NixOS/nix/blob/ee59af99f8619e17db4289843da62a24302d20b7/src/libexpr/eval.cc#L638
248 if (!DocComment)
249 return std::nullopt;
250
251 return ValueDescription{
252 .Doc = DocComment.getInnerText(State.positions),
253 .Arity = 0,
254 .Args = {},
255 };
256 }
257
258 return std::nullopt;
259}
260
261} // namespace
262
263AttrSetProvider::AttrSetProvider(std::unique_ptr<InboundPort> In,
264 std::unique_ptr<OutboundPort> Out)
265 : LSPServer(std::move(In), std::move(Out)),
266 State(new nix::EvalState({}, nix::openStore(), nix::fetchSettings,
267 nix::evalSettings)) {
268 Registry.addMethod(rpcMethod::EvalExpr, this, &AttrSetProvider::onEvalExpr);
269 Registry.addMethod(rpcMethod::AttrPathInfo, this,
271 Registry.addMethod(rpcMethod::AttrPathComplete, this,
273 Registry.addMethod(rpcMethod::OptionInfo, this,
275 Registry.addMethod(rpcMethod::OptionComplete, this,
277}
278
280 const std::string &Name,
281 lspserver::Callback<std::optional<std::string>> Reply) {
282 try {
283 nix::Expr *AST = state().parseExprFromString(Name, state().rootPath("."));
284 state().eval(AST, Nixpkgs);
285 Reply(std::nullopt);
286 return;
287 } catch (const nix::BaseError &Err) {
288 Reply(error(Err.info().msg.str()));
289 return;
290 } catch (const std::exception &Err) {
291 Reply(error(Err.what()));
292 return;
293 }
294}
295
297 const AttrPathInfoParams &AttrPath,
299 using RespT = AttrPathInfoResponse;
300 Reply([&]() -> llvm::Expected<RespT> {
301 try {
302 if (AttrPath.empty())
303 return error("attrpath is empty!");
304
305 nix::Value &V = nixt::selectStrings(state(), Nixpkgs, AttrPath);
306 state().forceValue(V, nix::noPos);
307 return RespT{
308 .Meta = metadataOf(state(), V),
309 .PackageDesc = describePackage(state(), V),
310 .ValueDesc = describeValue(state(), V),
311 };
312 } catch (const nix::BaseError &Err) {
313 return error(Err.info().msg.str());
314 } catch (const std::exception &Err) {
315 return error(Err.what());
316 }
317 }());
318}
319
321 const AttrPathCompleteParams &Params,
323 try {
324 nix::Value &Scope = nixt::selectStrings(state(), Nixpkgs, Params.Scope);
325
326 state().forceValue(Scope, nix::noPos);
327
328 if (Scope.type() != nix::ValueType::nAttrs) {
329 Reply(error("scope is not an attrset"));
330 return;
331 }
332
333 return Reply(completeNames(Scope, state(), Params.Prefix));
334 } catch (const nix::BaseError &Err) {
335 return Reply(error(Err.info().msg.str()));
336 } catch (const std::exception &Err) {
337 return Reply(error(Err.what()));
338 }
339}
340
342 const AttrPathInfoParams &AttrPath,
344 try {
345 if (AttrPath.empty()) {
346 Reply(error("attrpath is empty!"));
347 return;
348 }
349
350 nix::Value Option = nixt::selectOptions(
351 state(), Nixpkgs, nixt::toSymbols(state().symbols, AttrPath));
352
354
355 fillOptionDescription(state(), Option, R);
356
357 Reply(std::move(R));
358 return;
359 } catch (const nix::BaseError &Err) {
360 Reply(error(Err.info().msg.str()));
361 return;
362 } catch (const std::exception &Err) {
363 Reply(error(Err.what()));
364 return;
365 }
366}
367
369 const AttrPathCompleteParams &Params,
371 try {
372 nix::Value Scope = nixt::selectOptions(
373 state(), Nixpkgs, nixt::toSymbols(state().symbols, Params.Scope));
374
375 state().forceValue(Scope, nix::noPos);
376
377 if (Scope.type() != nix::ValueType::nAttrs) {
378 Reply(error("scope is not an attrset"));
379 return;
380 }
381
382 if (nixt::isOption(state(), Scope)) {
383 Reply(error("scope is already an option"));
384 return;
385 }
386
387 std::vector<OptionField> Response;
388
389 // FIXME: we may want to use "Trie" to speedup the string searching.
390 // However as my (roughtly) profiling the critical in this loop is
391 // evaluating package details.
392 // "Trie"s may not beneficial becausae it cannot speedup eval.
393 for (const auto *AttrPtr :
394 Scope.attrs()->lexicographicOrder(state().symbols)) {
395 const nix::Attr &Attr = *AttrPtr;
396 std::string_view Name = state().symbols[Attr.name];
397 if (Name.starts_with(Params.Prefix)) {
398 // Add a new "OptionField", see it's type.
399 assert(Attr.value);
400 OptionField NewField;
401 NewField.Name = Name;
402 if (nixt::isOption(state(), *Attr.value)) {
404 fillOptionDescription(state(), *Attr.value, Desc);
405 NewField.Description = std::move(Desc);
406 }
407 Response.emplace_back(std::move(NewField));
408 // We set this a very limited number as to speedup
409 if (Response.size() >= MaxItems)
410 break;
411 }
412 }
413 Reply(std::move(Response));
414 return;
415 } catch (const nix::BaseError &Err) {
416 Reply(error(Err.info().msg.str()));
417 return;
418 } catch (const std::exception &Err) {
419 Reply(error(Err.what()));
420 return;
421 }
422}
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:150
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::optional< std::string > Description
Definition AttrSet.h:129
std::optional< std::string > Example
Definition AttrSet.h:132
std::optional< OptionType > Type
Definition AttrSet.h:134
std::vector< lspserver::Location > Declarations
Definition AttrSet.h:130
std::optional< std::string > Default
Definition AttrSet.h:133
std::optional< OptionDescription > Description
Definition AttrSet.h:143
std::string Name
Definition AttrSet.h:142
std::optional< std::string > Description
Definition AttrSet.h:120
std::optional< std::string > Name
Definition AttrSet.h:121
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