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
65class VLACompletionProvider {
66 const VariableLookupAnalysis &VLA;
67
68 static CompletionItemKind getCompletionItemKind(const Definition &Def) {
69 if (Def.isBuiltin()) {
70 return CompletionItemKind::Keyword;
71 }
72 return CompletionItemKind::Variable;
73 }
74
75 /// Collect definition on some env, and also it's ancestors.
76 void collectDef(std::vector<CompletionItem> &Items, const EnvNode *Env,
77 const std::string &Prefix) {
78 if (!Env)
79 return;
80 collectDef(Items, Env->parent(), Prefix);
81 for (const auto &[Name, Def] : Env->defs()) {
82 if (Name.starts_with(
83 "__")) // These names are nix internal implementation, skip.
84 continue;
85 assert(Def);
86 if (Name.starts_with(Prefix)) {
87 addItem(Items, CompletionItem{
88 .label = Name,
89 .kind = getCompletionItemKind(*Def),
90 });
91 }
92 }
93 }
94
95public:
96 VLACompletionProvider(const VariableLookupAnalysis &VLA) : VLA(VLA) {}
97
98 /// Perform code completion right after this node.
99 void complete(const nixf::ExprVar &Desc, std::vector<CompletionItem> &Items,
100 const ParentMapAnalysis &PM) {
101 std::string Prefix = Desc.id().name();
102 collectDef(Items, upEnv(Desc, VLA, PM), Prefix);
103 }
104};
105
106/// \brief Provide completions by IPC. Asking nixpkgs provider.
107/// We simply select nixpkgs in separate process, thus this value does not need
108/// to be cached. (It is already cached in separate process.)
109///
110/// Currently, this procedure is explicitly blocked (synchronized). Because
111/// query nixpkgs value is relatively fast. In the future there might be nixd
112/// index, for performance.
113class NixpkgsCompletionProvider {
114
115 AttrSetClient &NixpkgsClient;
116
117public:
118 NixpkgsCompletionProvider(AttrSetClient &NixpkgsClient)
119 : NixpkgsClient(NixpkgsClient) {}
120
121 void resolvePackage(std::vector<std::string> Scope, std::string Name,
122 CompletionItem &Item) {
123 std::binary_semaphore Ready(0);
124 AttrPathInfoResponse Desc;
125 auto OnReply = [&Ready, &Desc](llvm::Expected<AttrPathInfoResponse> Resp) {
126 if (Resp)
127 Desc = *Resp;
128 Ready.release();
129 };
130 Scope.emplace_back(std::move(Name));
131 NixpkgsClient.attrpathInfo(Scope, std::move(OnReply));
132 Ready.acquire();
133 // Format "detail" and document.
134 const PackageDescription &PD = Desc.PackageDesc;
135 Item.documentation = MarkupContent{
136 .kind = MarkupKind::Markdown,
137 .value = PD.Description.value_or("") + "\n\n" +
138 PD.LongDescription.value_or(""),
139 };
140 Item.detail = PD.Version.value_or("?");
141 }
142
143 /// \brief Ask nixpkgs provider, give us a list of names. (thunks)
144 void completePackages(const lspserver::Range EditRange,
145 const AttrPathCompleteParams &Params,
146 std::vector<CompletionItem> &Items) {
147 std::binary_semaphore Ready(0);
148 std::vector<std::string> Names;
149 auto OnReply = [&Ready,
150 &Names](llvm::Expected<AttrPathCompleteResponse> Resp) {
151 if (!Resp) {
152 lspserver::elog("nixpkgs evaluator reported: {0}", Resp.takeError());
153 Ready.release();
154 return;
155 }
156 Names = *Resp; // Copy response to waiting thread.
157 Ready.release();
158 };
159 // Send request.
160 NixpkgsClient.attrpathComplete(Params, std::move(OnReply));
161 Ready.acquire();
162 // Now we have "Names", use these to fill "Items".
163 for (const auto &Name : Names) {
164 if (Name.starts_with(Params.Prefix)) {
165 addItem(Items, CompletionItem{
166 .label = Name,
167 .kind = CompletionItemKind::Field,
168 .textEdit = lspserver::TextEdit{.range = EditRange,
169 .newText = Name},
170 .data = llvm::formatv("{0}", toJSON(Params)),
171 });
172 }
173 }
174 }
175};
176
177/// \brief Provide completion list by nixpkgs module system (options).
178class OptionCompletionProvider {
179 AttrSetClient &OptionClient;
180
181 // Where is the module set. (e.g. nixos)
182 std::string ModuleOrigin;
183
184 // Wheter the client support code snippets.
185 bool ClientSupportSnippet;
186
187 static std::string escapeCharacters(const std::set<char> &Charset,
188 const std::string &Origin) {
189 // Escape characters listed in charset.
190 std::string Ret;
191 Ret.reserve(Origin.size());
192 for (const auto Ch : Origin) {
193 if (Charset.contains(Ch)) {
194 Ret += "\\";
195 Ret += Ch;
196 } else {
197 Ret += Ch;
198 }
199 }
200 return Ret;
201 }
202
203 void fillInsertText(CompletionItem &Item, const std::string &Name,
204 const std::string &Value) const {
205 if (!ClientSupportSnippet) {
206 Item.insertTextFormat = InsertTextFormat::PlainText;
207 Item.insertText = Name + " = " + Value + ";";
208 return;
209 }
210 Item.insertTextFormat = InsertTextFormat::Snippet;
211 Item.insertText = Name + " = " +
212 "${1:" + escapeCharacters({'\\', '$', '}'}, Value) + "}" +
213 ";";
214 }
215
216public:
217 OptionCompletionProvider(AttrSetClient &OptionClient,
218 std::string ModuleOrigin, bool ClientSupportSnippet)
219 : OptionClient(OptionClient), ModuleOrigin(std::move(ModuleOrigin)),
220 ClientSupportSnippet(ClientSupportSnippet) {}
221
222 void completeOptions(const lspserver::Range EditRange,
223 std::vector<std::string> Scope, std::string Prefix,
224 std::vector<CompletionItem> &Items) {
225 std::binary_semaphore Ready(0);
227 auto OnReply = [&Ready,
228 &Names](llvm::Expected<OptionCompleteResponse> Resp) {
229 if (!Resp) {
230 lspserver::elog("option worker reported: {0}", Resp.takeError());
231 Ready.release();
232 return;
233 }
234 Names = *Resp; // Copy response to waiting thread.
235 Ready.release();
236 };
237 // Send request.
238 AttrPathCompleteParams Params{std::move(Scope), std::move(Prefix)};
239 OptionClient.optionComplete(Params, std::move(OnReply));
240 Ready.acquire();
241 // Now we have "Names", use these to fill "Items".
242 //
243 // When Params.Prefix is empty, the cursor is inside an empty hole and
244 // EditRange does not point at a real prefix to replace, so we omit the
245 // textEdit and let the editor fall back to insertText/label.
246 bool HasPrefix = !Params.Prefix.empty();
247 auto MkTextEdit =
248 [&](llvm::StringRef NewText) -> std::optional<lspserver::TextEdit> {
249 if (!HasPrefix)
250 return std::nullopt;
251 return lspserver::TextEdit{.range = EditRange, .newText = NewText.str()};
252 };
253 for (const nixd::OptionField &Field : Names) {
254 if (!Field.Description) {
255 addItem(Items, CompletionItem{
256 .label = Field.Name,
257 .kind = OptionAttrKind,
258 .detail = ModuleOrigin,
259 .textEdit = MkTextEdit(Field.Name),
260 });
261 continue;
262 }
263
264 const OptionDescription &Desc = *Field.Description;
265
266 // Build the shared bits (detail, documentation) once, then emit one
267 // completion item per value source (`example`, `default`). This lets
268 // the user pick between the sample code the option author suggested
269 // and its default value - useful when an option only has one of the
270 // two, or when the user wants the default as a starting point.
271 std::string TypeDetail = ModuleOrigin + " | ";
272 if (Desc.Type) {
273 std::string TypeName = Desc.Type->Name.value_or("");
274 std::string TypeDesc = Desc.Type->Description.value_or("");
275 TypeDetail += llvm::formatv("{0} ({1})", TypeName, TypeDesc);
276 } else {
277 TypeDetail += "? (missing type)";
278 }
279 MarkupContent Doc{
280 .kind = MarkupKind::Markdown,
281 .value = Desc.Description.value_or(""),
282 };
283
284 // Check whether both variants will be emitted so we can decide
285 // whether to disambiguate the labels with "(example)" / "(default)".
286 bool HasExample = Desc.Example.has_value();
287 // Only emit a separate `default` item when it would differ from the
288 // example; otherwise the user would see two identical snippets.
289 bool HasDefault =
290 Desc.Default.has_value() && Desc.Default != Desc.Example;
291 bool HasBoth = HasExample && HasDefault;
292
293 auto emit = [&](const std::string &Value, llvm::StringRef Source,
294 llvm::StringRef SortPrefix) {
295 // When both variants exist, append the source so users can tell
296 // them apart. `filterText` is always the plain option name so
297 // typing the name matches both items.
298 std::string Label =
299 HasBoth ? llvm::formatv("{0} ({1})", Field.Name, Source).str()
300 : Field.Name;
301 CompletionItem Item{
302 .label = std::move(Label),
303 .kind = OptionKind,
304 .detail = TypeDetail,
305 .documentation = Doc,
306 .sortText = (SortPrefix + Field.Name).str(),
307 .filterText = Field.Name,
308 };
309 fillInsertText(Item, Field.Name, Value);
310 Item.textEdit = MkTextEdit(Item.insertText);
311 addItem(Items, std::move(Item));
312 };
313
314 bool Emitted = false;
315 if (HasExample) {
316 emit(*Desc.Example, "example", "0");
317 Emitted = true;
318 }
319 if (HasDefault) {
320 emit(*Desc.Default, "default", "1");
321 Emitted = true;
322 }
323 if (!Emitted) {
324 // No example or default to insert — still offer the option name
325 // as a bare completion so users can discover it.
326 CompletionItem Item{
327 .label = Field.Name,
328 .kind = OptionKind,
329 .detail = TypeDetail,
330 .documentation = Doc,
331 };
332 fillInsertText(Item, Field.Name, "");
333 Item.textEdit = MkTextEdit(Item.insertText);
334 addItem(Items, std::move(Item));
335 }
336 }
337 }
338};
339
340void completeAttrName(const lspserver::Range EditRange,
341 const std::vector<std::string> &Scope,
342 const std::string &Prefix,
343 Controller::OptionMapTy &Options, bool CompletionSnippets,
344 std::vector<CompletionItem> &List) {
345 for (const auto &[Name, Provider] : Options) {
346 AttrSetClient *Client = Options.at(Name)->client();
347 if (!Client) [[unlikely]] {
348 elog("skipped client {0} as it is dead", Name);
349 continue;
350 }
351 OptionCompletionProvider OCP(*Client, Name, CompletionSnippets);
352 OCP.completeOptions(EditRange, Scope, Prefix, List);
353 }
354}
355
356void completeAttrPath(const lspserver::Range EditRange, const Node &N,
357 const ParentMapAnalysis &PM, std::mutex &OptionsLock,
358 Controller::OptionMapTy &Options, bool Snippets,
359 std::vector<lspserver::CompletionItem> &Items) {
360 std::vector<std::string> Scope;
361 using PathResult = FindAttrPathResult;
362 auto R = findAttrPathForOptions(N, PM, Scope);
363 if (R == PathResult::OK) {
364 // Construct request.
365 std::string Prefix = Scope.back();
366 Scope.pop_back();
367 {
368 std::lock_guard _(OptionsLock);
369 completeAttrName(EditRange, Scope, Prefix, Options, Snippets, Items);
370 }
371 }
372}
373
374AttrPathCompleteParams mkParams(nixd::Selector Sel, bool IsComplete) {
375 if (IsComplete || Sel.empty()) {
376 return {
377 .Scope = std::move(Sel),
378 .Prefix = "",
379 };
380 }
381 std::string Back = std::move(Sel.back());
382 Sel.pop_back();
383 return {
384 .Scope = Sel,
385 .Prefix = std::move(Back),
386 };
387}
388
389#define DBG DBGPREFIX ": "
390
391void completeVarName(const lspserver::Range EditRange,
392 const VariableLookupAnalysis &VLA,
393 const ParentMapAnalysis &PM, const nixf::ExprVar &N,
394 AttrSetClient &Client, std::vector<CompletionItem> &List) {
395#define DBGPREFIX "completion/var"
396
397 VLACompletionProvider VLAP(VLA);
398 VLAP.complete(N, List, PM);
399
400 // Try to complete the name by known idioms.
401 try {
402 Selector Sel = idioms::mkVarSelector(N, VLA, PM);
403
404 // Clickling "pkgs" does not make sense for variable completion
405 if (Sel.empty())
406 return;
407
408 // Invoke nixpkgs provider to get the completion list.
409 NixpkgsCompletionProvider NCP(Client);
410 // Variable names are always incomplete.
411 NCP.completePackages(EditRange, mkParams(Sel, /*IsComplete=*/false), List);
412 } catch (ExceedSizeError &) {
413 // Let "onCompletion" catch this exception to set "inComplete" field.
414 throw;
415 } catch (std::exception &E) {
416 return log(DBG "skipped, reason: {0}", E.what());
417 }
418
419#undef DBGPREFIX
420}
421
422/// \brief Complete a "select" expression.
423/// \param IsComplete Whether or not the last element of the selector is
424/// effectively incomplete.
425/// e.g.
426/// - incomplete: `lib.gen|`
427/// - complete: `lib.attrset.|`
428void completeSelect(const lspserver::Range EditRange,
429 const nixf::ExprSelect &Select, AttrSetClient &Client,
431 const nixf::ParentMapAnalysis &PM, bool IsComplete,
432 std::vector<CompletionItem> &List) {
433#define DBGPREFIX "completion/select"
434 // The base expr for selecting.
435 const nixf::Expr &BaseExpr = Select.expr();
436
437 // Determine that the name is one of special names interesting
438 // for nix language. If it is not a simple variable, skip this
439 // case.
440 if (BaseExpr.kind() != Node::NK_ExprVar) {
441 return;
442 }
443
444 const auto &Var = static_cast<const nixf::ExprVar &>(BaseExpr);
445 // Ask nixpkgs provider to get idioms completion.
446 NixpkgsCompletionProvider NCP(Client);
447
448 try {
449 Selector Sel =
450 idioms::mkSelector(Select, idioms::mkVarSelector(Var, VLA, PM));
451 NCP.completePackages(EditRange, mkParams(Sel, IsComplete), List);
452 } catch (ExceedSizeError &) {
453 // Let "onCompletion" catch this exception to set "inComplete" field.
454 throw;
455 } catch (std::exception &E) {
456 return log(DBG "skipped, reason: {0}", E.what());
457 }
458
459#undef DBGPREFIX
460}
461
462} // namespace
463
464void Controller::onCompletion(const CompletionParams &Params,
466 using CheckTy = CompletionList;
467 auto Action = [Reply = std::move(Reply), URI = Params.textDocument.uri,
468 Pos = toNixfPosition(Params.position), this]() mutable {
469 const auto File = URI.file().str();
470 return Reply([&]() -> llvm::Expected<CompletionList> {
471 const auto TU = CheckDefault(getTU(File));
472 const auto AST = CheckDefault(getAST(*TU));
473
474 const auto *Desc = AST->descend({Pos, Pos});
475 CheckDefault(Desc && canCompleteAt(*Desc));
476
477 const auto &N = *Desc;
478 const auto &PM = *TU->parentMap();
479 const auto &UpExpr = *CheckDefault(PM.upExpr(N));
480
481 lspserver::Range EditRange = toLSPRange(TU->src(), N.range());
482 if (N.kind() == Node::NK_Dot) {
483 // If the node is a dot, insert after the dot
484 EditRange.start = EditRange.end;
485 }
486
487 return [&]() {
488 CompletionList List;
489 const VariableLookupAnalysis &VLA = *TU->variableLookup();
490 try {
491 switch (UpExpr.kind()) {
492 // In these cases, assume the cursor have "variable" scoping.
493 case Node::NK_ExprVar: {
494 completeVarName(EditRange, VLA, PM,
495 static_cast<const nixf::ExprVar &>(UpExpr),
496 *nixpkgsClient(), List.items);
497 return List;
498 }
499 // A "select" expression. e.g.
500 // foo.a|
501 // foo.|
502 // foo.a.bar|
503 case Node::NK_ExprSelect: {
504 const auto &Select = static_cast<const nixf::ExprSelect &>(UpExpr);
505 completeSelect(EditRange, Select, *nixpkgsClient(), VLA, PM,
506 N.kind() == Node::NK_Dot, List.items);
507 return List;
508 }
509 case Node::NK_ExprAttrs: {
510 completeAttrPath(EditRange, N, PM, OptionsLock, Options,
511 ClientCaps.CompletionSnippets, List.items);
512 return List;
513 }
514 default:
515 return List;
516 }
517 } catch (ExceedSizeError &Err) {
518 List.isIncomplete = true;
519 return List;
520 }
521 }();
522 }());
523 };
524 boost::asio::post(Pool, std::move(Action));
525}
526
527void Controller::onCompletionItemResolve(const CompletionItem &Params,
529
530 auto Action = [Params, Reply = std::move(Reply), this]() mutable {
531 if (Params.data.empty()) {
532 Reply(Params);
533 return;
534 }
535 AttrPathCompleteParams Req;
536 auto EV = llvm::json::parse(Params.data);
537 if (!EV) {
538 // If the json value cannot be parsed, this is very unlikely to happen.
539 Reply(EV.takeError());
540 return;
541 }
542
543 llvm::json::Path::Root Root;
544 fromJSON(*EV, Req, Root);
545
546 // FIXME: handle null nixpkgsClient()
547 NixpkgsCompletionProvider NCP(*nixpkgsClient());
548 CompletionItem Resp = Params;
549 NCP.resolvePackage(Req.Scope, Params.label, Resp);
550
551 Reply(std::move(Resp));
552 };
553 boost::asio::post(Pool, std::move(Action));
554}
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:56
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::vector< OptionField > OptionCompleteResponse
Definition AttrSet.h:152
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
PackageDescription PackageDesc
Package description of the attribute path, if available.
Definition AttrSet.h:98
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::optional< std::string > Default
Definition AttrSet.h:133
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