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 "Convert.h"
8
10
13
15
16#include <boost/asio/post.hpp>
17
18#include <exception>
19#include <semaphore>
20#include <set>
21#include <unordered_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
35CompletionItemKind OptionKind = CompletionItemKind::Constructor;
36CompletionItemKind OptionAttrKind = CompletionItemKind::Class;
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
51class VLACompletionProvider {
52 const VariableLookupAnalysis &VLA;
53
54 static CompletionItemKind getCompletionItemKind(const Definition &Def) {
55 if (Def.isBuiltin()) {
56 return CompletionItemKind::Keyword;
57 }
58 return CompletionItemKind::Variable;
59 }
60
61 /// Collect definition on some env, and also it's ancestors.
62 void collectDef(std::vector<CompletionItem> &Items, const EnvNode *Env,
63 const std::string &Prefix) {
64 if (!Env)
65 return;
66 collectDef(Items, Env->parent(), Prefix);
67 for (const auto &[Name, Def] : Env->defs()) {
68 if (Name.starts_with(
69 "__")) // These names are nix internal implementation, skip.
70 continue;
71 assert(Def);
72 if (Name.starts_with(Prefix)) {
73 addItem(Items, CompletionItem{
74 .label = Name,
75 .kind = getCompletionItemKind(*Def),
76 });
77 }
78 }
79 }
80
81public:
82 VLACompletionProvider(const VariableLookupAnalysis &VLA) : VLA(VLA) {}
83
84 /// Perform code completion right after this node.
85 void complete(const nixf::ExprVar &Desc, std::vector<CompletionItem> &Items,
86 const ParentMapAnalysis &PM) {
87 std::string Prefix = Desc.id().name();
88 collectDef(Items, upEnv(Desc, VLA, PM), Prefix);
89 }
90};
91
92/// \brief Provide completions by IPC. Asking nixpkgs provider.
93/// We simply select nixpkgs in separate process, thus this value does not need
94/// to be cached. (It is already cached in separate process.)
95///
96/// Currently, this procedure is explicitly blocked (synchronized). Because
97/// query nixpkgs value is relatively fast. In the future there might be nixd
98/// index, for performance.
99class NixpkgsCompletionProvider {
100
101 AttrSetClient &NixpkgsClient;
102
103public:
104 NixpkgsCompletionProvider(AttrSetClient &NixpkgsClient)
105 : NixpkgsClient(NixpkgsClient) {}
106
107 void resolvePackage(std::vector<std::string> Scope, std::string Name,
108 CompletionItem &Item) {
109 std::binary_semaphore Ready(0);
111 auto OnReply = [&Ready, &Desc](llvm::Expected<AttrPathInfoResponse> Resp) {
112 if (Resp)
113 Desc = *Resp;
114 Ready.release();
115 };
116 Scope.emplace_back(std::move(Name));
117 NixpkgsClient.attrpathInfo(Scope, std::move(OnReply));
118 Ready.acquire();
119 // Format "detail" and document.
120 const PackageDescription &PD = Desc.PackageDesc;
122 .kind = MarkupKind::Markdown,
123 .value = PD.Description.value_or("") + "\n\n" +
124 PD.LongDescription.value_or(""),
125 };
126 Item.detail = PD.Version.value_or("?");
127 }
128
129 /// \brief Ask nixpkgs provider, give us a list of names. (thunks)
130 void completePackages(const AttrPathCompleteParams &Params,
131 std::vector<CompletionItem> &Items) {
132 std::binary_semaphore Ready(0);
133 std::vector<std::string> Names;
134 auto OnReply = [&Ready,
135 &Names](llvm::Expected<AttrPathCompleteResponse> Resp) {
136 if (!Resp) {
137 lspserver::elog("nixpkgs evaluator reported: {0}", Resp.takeError());
138 Ready.release();
139 return;
140 }
141 Names = *Resp; // Copy response to waiting thread.
142 Ready.release();
143 };
144 // Send request.
145 NixpkgsClient.attrpathComplete(Params, std::move(OnReply));
146 Ready.acquire();
147 // Now we have "Names", use these to fill "Items".
148 for (const auto &Name : Names) {
149 if (Name.starts_with(Params.Prefix)) {
150 addItem(Items, CompletionItem{
151 .label = Name,
152 .kind = CompletionItemKind::Field,
153 .data = llvm::formatv("{0}", toJSON(Params)),
154 });
155 }
156 }
157 }
158};
159
160/// \brief Provide completion list by nixpkgs module system (options).
161class OptionCompletionProvider {
162 AttrSetClient &OptionClient;
163
164 // Where is the module set. (e.g. nixos)
165 std::string ModuleOrigin;
166
167 // Wheter the client support code snippets.
168 bool ClientSupportSnippet;
169
170 static std::string escapeCharacters(const std::set<char> &Charset,
171 const std::string &Origin) {
172 // Escape characters listed in charset.
173 std::string Ret;
174 Ret.reserve(Origin.size());
175 for (const auto Ch : Origin) {
176 if (Charset.contains(Ch)) {
177 Ret += "\\";
178 Ret += Ch;
179 } else {
180 Ret += Ch;
181 }
182 }
183 return Ret;
184 }
185
186 void fillInsertText(CompletionItem &Item, const std::string &Name,
187 const OptionDescription &Desc) const {
188 if (!ClientSupportSnippet) {
189 Item.insertTextFormat = InsertTextFormat::PlainText;
190 Item.insertText = Name + " = " + Desc.Example.value_or("") + ";";
191 return;
192 }
193 Item.insertTextFormat = InsertTextFormat::Snippet;
194 Item.insertText =
195 Name + " = " +
196 "${1:" + escapeCharacters({'\\', '$', '}'}, Desc.Example.value_or("")) +
197 "}" + ";";
198 }
199
200public:
201 OptionCompletionProvider(AttrSetClient &OptionClient,
202 std::string ModuleOrigin, bool ClientSupportSnippet)
203 : OptionClient(OptionClient), ModuleOrigin(std::move(ModuleOrigin)),
204 ClientSupportSnippet(ClientSupportSnippet) {}
205
206 void completeOptions(std::vector<std::string> Scope, std::string Prefix,
207 std::vector<CompletionItem> &Items) {
208 std::binary_semaphore Ready(0);
210 auto OnReply = [&Ready,
211 &Names](llvm::Expected<OptionCompleteResponse> Resp) {
212 if (!Resp) {
213 lspserver::elog("option worker reported: {0}", Resp.takeError());
214 Ready.release();
215 return;
216 }
217 Names = *Resp; // Copy response to waiting thread.
218 Ready.release();
219 };
220 // Send request.
221 AttrPathCompleteParams Params{std::move(Scope), std::move(Prefix)};
222 OptionClient.optionComplete(Params, std::move(OnReply));
223 Ready.acquire();
224 // Now we have "Names", use these to fill "Items".
225 for (const nixd::OptionField &Field : Names) {
226 CompletionItem Item;
227
228 Item.label = Field.Name;
229 Item.detail = ModuleOrigin;
230
231 if (Field.Description) {
232 const OptionDescription &Desc = *Field.Description;
233 Item.kind = OptionKind;
234 fillInsertText(Item, Field.Name, Desc);
236 .kind = MarkupKind::Markdown,
237 .value = Desc.Description.value_or(""),
238 };
239 Item.detail += " | "; // separater between origin and type desc.
240 if (Desc.Type) {
241 std::string TypeName = Desc.Type->Name.value_or("");
242 std::string TypeDesc = Desc.Type->Description.value_or("");
243 Item.detail += llvm::formatv("{0} ({1})", TypeName, TypeDesc);
244 } else {
245 Item.detail += "? (missing type)";
246 }
247 addItem(Items, std::move(Item));
248 } else {
249 Item.kind = OptionAttrKind;
250 addItem(Items, std::move(Item));
251 }
252 }
253 }
254};
255
256void completeAttrName(const std::vector<std::string> &Scope,
257 const std::string &Prefix,
258 Controller::OptionMapTy &Options, bool CompletionSnippets,
259 std::vector<CompletionItem> &List) {
260 for (const auto &[Name, Provider] : Options) {
261 AttrSetClient *Client = Options.at(Name)->client();
262 if (!Client) [[unlikely]] {
263 elog("skipped client {0} as it is dead", Name);
264 continue;
265 }
266 OptionCompletionProvider OCP(*Client, Name, CompletionSnippets);
267 OCP.completeOptions(Scope, Prefix, List);
268 }
269}
270
271void completeAttrPath(const Node &N, const ParentMapAnalysis &PM,
272 std::mutex &OptionsLock, Controller::OptionMapTy &Options,
273 bool Snippets,
274 std::vector<lspserver::CompletionItem> &Items) {
275 std::vector<std::string> Scope;
276 using PathResult = FindAttrPathResult;
277 auto R = findAttrPath(N, PM, Scope);
278 if (R == PathResult::OK) {
279 // Construct request.
280 std::string Prefix = Scope.back();
281 Scope.pop_back();
282 {
283 std::lock_guard _(OptionsLock);
284 completeAttrName(Scope, Prefix, Options, Snippets, Items);
285 }
286 }
287}
288
289AttrPathCompleteParams mkParams(nixd::Selector Sel, bool IsComplete) {
290 if (IsComplete || Sel.empty()) {
291 return {
292 .Scope = std::move(Sel),
293 .Prefix = "",
294 };
295 }
296 std::string Back = std::move(Sel.back());
297 Sel.pop_back();
298 return {
299 .Scope = Sel,
300 .Prefix = std::move(Back),
301 };
302}
303
304#define DBG DBGPREFIX ": "
305
306void completeVarName(const VariableLookupAnalysis &VLA,
307 const ParentMapAnalysis &PM, const nixf::ExprVar &N,
308 AttrSetClient &Client, std::vector<CompletionItem> &List) {
309#define DBGPREFIX "completion/var"
310
311 VLACompletionProvider VLAP(VLA);
312 VLAP.complete(N, List, PM);
313
314 // Try to complete the name by known idioms.
315 try {
316 Selector Sel = idioms::mkVarSelector(N, VLA, PM);
317
318 // Clickling "pkgs" does not make sense for variable completion
319 if (Sel.empty())
320 return;
321
322 // Invoke nixpkgs provider to get the completion list.
323 NixpkgsCompletionProvider NCP(Client);
324 // Variable names are always incomplete.
325 NCP.completePackages(mkParams(Sel, /*IsComplete=*/false), List);
326 } catch (ExceedSizeError &) {
327 // Let "onCompletion" catch this exception to set "inComplete" field.
328 throw;
329 } catch (std::exception &E) {
330 return log(DBG "skipped, reason: {0}", E.what());
331 }
332
333#undef DBGPREFIX
334}
335
336/// \brief Complete a "select" expression.
337/// \param IsComplete Whether or not the last element of the selector is
338/// effectively incomplete.
339/// e.g.
340/// - incomplete: `lib.gen|`
341/// - complete: `lib.attrset.|`
342void completeSelect(const nixf::ExprSelect &Select, AttrSetClient &Client,
344 const nixf::ParentMapAnalysis &PM, bool IsComplete,
345 std::vector<CompletionItem> &List) {
346#define DBGPREFIX "completion/select"
347 // The base expr for selecting.
348 const nixf::Expr &BaseExpr = Select.expr();
349
350 // Determine that the name is one of special names interesting
351 // for nix language. If it is not a simple variable, skip this
352 // case.
353 if (BaseExpr.kind() != Node::NK_ExprVar) {
354 return;
355 }
356
357 const auto &Var = static_cast<const nixf::ExprVar &>(BaseExpr);
358 // Ask nixpkgs provider to get idioms completion.
359 NixpkgsCompletionProvider NCP(Client);
360
361 try {
362 Selector Sel =
363 idioms::mkSelector(Select, idioms::mkVarSelector(Var, VLA, PM));
364 NCP.completePackages(mkParams(Sel, IsComplete), List);
365 } catch (ExceedSizeError &) {
366 // Let "onCompletion" catch this exception to set "inComplete" field.
367 throw;
368 } catch (std::exception &E) {
369 return log(DBG "skipped, reason: {0}", E.what());
370 }
371
372#undef DBGPREFIX
373}
374
375} // namespace
376
377void Controller::onCompletion(const CompletionParams &Params,
379 auto Action = [Reply = std::move(Reply), URI = Params.textDocument.uri,
380 Pos = toNixfPosition(Params.position), this]() mutable {
381 std::string File(URI.file());
382 if (std::shared_ptr<NixTU> TU = getTU(File, Reply)) [[likely]] {
383 if (std::shared_ptr<nixf::Node> AST = getAST(*TU, Reply)) [[likely]] {
384 const nixf::Node *Desc = AST->descend({Pos, Pos});
385 if (!Desc) {
386 Reply(error("cannot find corresponding node on given position"));
387 return;
388 }
389 if (!Desc->children().empty()) {
390 Reply(CompletionList{});
391 return;
392 }
393 const nixf::Node &N = *Desc;
394 const ParentMapAnalysis &PM = *TU->parentMap();
395 const Node *MaybeUpExpr = PM.upExpr(N);
396 if (!MaybeUpExpr) {
397 // If there is no concrete expression containing the cursor
398 // Reply an empty list.
399 Reply(CompletionList{});
400 return;
401 }
402 // Otherwise, construct the completion list from a set of providers.
403 const Node &UpExpr = *MaybeUpExpr;
404 Reply([&]() -> CompletionList {
405 CompletionList List;
406 const VariableLookupAnalysis &VLA = *TU->variableLookup();
407 try {
408 switch (UpExpr.kind()) {
409 // In these cases, assume the cursor have "variable" scoping.
410 case Node::NK_ExprVar: {
411 completeVarName(VLA, PM,
412 static_cast<const nixf::ExprVar &>(UpExpr),
413 *nixpkgsClient(), List.items);
414 break;
415 }
416 // A "select" expression. e.g.
417 // foo.a|
418 // foo.|
419 // foo.a.bar|
420 case Node::NK_ExprSelect: {
421 const auto &Select =
422 static_cast<const nixf::ExprSelect &>(UpExpr);
423 completeSelect(Select, *nixpkgsClient(), VLA, PM,
424 N.kind() == Node::NK_Dot, List.items);
425 break;
426 }
427 case Node::NK_ExprAttrs: {
428 completeAttrPath(N, PM, OptionsLock, Options,
429 ClientCaps.CompletionSnippets, List.items);
430 break;
431 }
432 default:
433 break;
434 }
435 } catch (ExceedSizeError &Err) {
436 List.isIncomplete = true;
437 }
438 return List;
439 }());
440 }
441 }
442 };
443 boost::asio::post(Pool, std::move(Action));
444}
445
446void Controller::onCompletionItemResolve(const CompletionItem &Params,
448
449 auto Action = [Params, Reply = std::move(Reply), this]() mutable {
450 if (Params.data.empty()) {
451 Reply(Params);
452 return;
453 }
455 auto EV = llvm::json::parse(Params.data);
456 if (!EV) {
457 // If the json value cannot be parsed, this is very unlikely to happen.
458 Reply(EV.takeError());
459 return;
460 }
461
462 llvm::json::Path::Root Root;
463 fromJSON(*EV, Req, Root);
464
465 // FIXME: handle null nixpkgsClient()
466 NixpkgsCompletionProvider NCP(*nixpkgsClient());
467 CompletionItem Resp = Params;
468 NCP.resolvePackage(Req.Scope, Params.label, Resp);
469
470 Reply(std::move(Resp));
471 };
472 boost::asio::post(Pool, std::move(Action));
473}
This file declares some common analysis (tree walk) on the AST.
Types used in nixpkgs provider.
#define DBG
Convert between LSP and nixf types.
Lookup variable names, from it's parent scope.
void attrpathInfo(const AttrPathInfoParams &Params, lspserver::Callback< AttrPathInfoResponse > Reply)
void attrpathComplete(const AttrPathCompleteParams &Params, lspserver::Callback< AttrPathCompleteResponse > Reply)
void optionComplete(const AttrPathCompleteParams &Params, lspserver::Callback< OptionCompleteResponse > Reply)
std::map< std::string, std::unique_ptr< AttrSetClientProc > > OptionMapTy
Definition Controller.h:21
Represents a definition.
bool isBuiltin() const
A set of variable definitions, which may inherit parent environment.
EnvNode * parent() const
Expr & expr() const
Definition Expr.h:20
const Identifier & id() const
Definition Simple.h:200
const std::string & name() const
Definition Basic.h:120
NodeKind kind() const
Definition Basic.h:34
virtual ChildVector children() const =0
const Node * upExpr(const Node &N) const
Search up until the node becomes a concrete expression. a ^<--— ID -> ExprVar.
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
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:54
FindAttrPathResult findAttrPath(const nixf::Node &N, const nixf::ParentMapAnalysis &PM, std::vector< std::string > &Path)
Heuristically find attrpath suitable for "attrpath" completion.
Definition AST.cpp:283
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:42
FindAttrPathResult
Definition AST.h:119
nixf::Position toNixfPosition(const lspserver::Position &P)
Definition Convert.cpp:32
std::vector< OptionField > OptionCompleteResponse
Definition AttrSet.h:137
std::optional< MarkupContent > documentation
A human-readable string that represents a doc-comment.
Represents a collection of completion items to be presented in the editor.
std::vector< CompletionItem > items
The completion items.
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:96
PackageDescription PackageDesc
Package description of the attribute path, if available.
Definition AttrSet.h:86
std::optional< std::string > Description
Definition AttrSet.h:115
std::optional< std::string > Example
Definition AttrSet.h:118
std::optional< OptionType > Type
Definition AttrSet.h:119
std::optional< std::string > Version
Definition AttrSet.h:52
std::optional< std::string > Description
Definition AttrSet.h:53
std::optional< std::string > LongDescription
Definition AttrSet.h:54