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