1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qqmlcompletionsupport_p.h"
5#include "qqmllsutils_p.h"
6
7#include <QtLanguageServer/private/qlanguageserverspectypes_p.h>
8#include <QtCore/qthreadpool.h>
9#include <QtCore/private/qduplicatetracker_p.h>
10#include <QtCore/QRegularExpression>
11#include <QtQmlDom/private/qqmldomexternalitems_p.h>
12#include <QtQmlDom/private/qqmldomtop_p.h>
13
14QT_BEGIN_NAMESPACE
15using namespace QLspSpecification;
16using namespace QQmlJS::Dom;
17using namespace Qt::StringLiterals;
18
19Q_LOGGING_CATEGORY(complLog, "qt.languageserver.completions")
20
21bool CompletionRequest::fillFrom(QmlLsp::OpenDocument doc, const Parameters &params,
22 Response &&response)
23{
24 // do not call BaseRequest::fillFrom() to avoid taking the Mutex twice and getting an
25 // inconsistent state.
26 m_parameters = params;
27 m_response = std::move(response);
28
29 if (!doc.textDocument)
30 return false;
31
32 std::optional<int> targetVersion;
33 {
34 QMutexLocker l(doc.textDocument->mutex());
35 targetVersion = doc.textDocument->version();
36 code = doc.textDocument->toPlainText();
37 }
38 m_minVersion = (targetVersion ? *targetVersion : 0);
39
40 return true;
41}
42
43void QmlCompletionSupport::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol)
44{
45 protocol->registerCompletionRequestHandler(handler: getRequestHandler());
46 protocol->registerCompletionItemResolveRequestHandler(
47 handler: [](const QByteArray &, const CompletionItem &cParams,
48 LSPResponse<CompletionItem> &&response) { response.sendResponse(r: cParams); });
49}
50
51QString QmlCompletionSupport::name() const
52{
53 return u"QmlCompletionSupport"_s;
54}
55
56void QmlCompletionSupport::setupCapabilities(
57 const QLspSpecification::InitializeParams &,
58 QLspSpecification::InitializeResult &serverCapabilities)
59{
60 QLspSpecification::CompletionOptions cOptions;
61 if (serverCapabilities.capabilities.completionProvider)
62 cOptions = *serverCapabilities.capabilities.completionProvider;
63 cOptions.resolveProvider = false;
64 cOptions.triggerCharacters = QList<QByteArray>({ QByteArray(".") });
65 serverCapabilities.capabilities.completionProvider = cOptions;
66}
67
68void QmlCompletionSupport::process(RequestPointerArgument req)
69{
70 QmlLsp::OpenDocumentSnapshot doc =
71 m_codeModel->snapshotByUrl(url: req->m_parameters.textDocument.uri);
72 req->sendCompletions(doc);
73}
74
75QString CompletionRequest::urlAndPos() const
76{
77 return QString::fromUtf8(ba: m_parameters.textDocument.uri) + u":"
78 + QString::number(m_parameters.position.line) + u":"
79 + QString::number(m_parameters.position.character);
80}
81
82// finds the filter string, the base (for fully qualified accesses) and the whole string
83// just before pos in code
84struct CompletionContextStrings
85{
86 CompletionContextStrings(QString code, qsizetype pos);
87
88public:
89 // line up until pos
90 QStringView preLine() const
91 {
92 return QStringView(m_code).mid(pos: m_lineStart, n: m_pos - m_lineStart);
93 }
94 // the part used to filter the completion (normally actual filtering is left to the client)
95 QStringView filterChars() const
96 {
97 return QStringView(m_code).mid(pos: m_filterStart, n: m_pos - m_filterStart);
98 }
99 // the base part (qualified access)
100 QStringView base() const
101 {
102 return QStringView(m_code).mid(pos: m_baseStart, n: m_filterStart - m_baseStart);
103 }
104 // if we are at line start
105 bool atLineStart() const { return m_atLineStart; }
106
107private:
108 QString m_code; // the current code
109 qsizetype m_pos = {}; // current position of the cursor
110 qsizetype m_filterStart = {}; // start of the characters that are used to filter the suggestions
111 qsizetype m_lineStart = {}; // start of the current line
112 qsizetype m_baseStart = {}; // start of the dotted expression that ends at the cursor position
113 bool m_atLineStart = {}; // if there are only spaces before base
114};
115
116CompletionContextStrings::CompletionContextStrings(QString code, qsizetype pos)
117 : m_code(code), m_pos(pos)
118{
119 // computes the context just before pos in code.
120 // After this code all the values of all the attributes should be correct (see above)
121 // handle also letter or numbers represented a surrogate pairs?
122 m_filterStart = m_pos;
123 while (m_filterStart != 0) {
124 QChar c = code.at(i: m_filterStart - 1);
125 if (!c.isLetterOrNumber() && c != u'_')
126 break;
127 else
128 --m_filterStart;
129 }
130 // handle spaces?
131 m_baseStart = m_filterStart;
132 while (m_baseStart != 0) {
133 QChar c = code.at(i: m_baseStart - 1);
134 if (c != u'.' || m_baseStart == 1)
135 break;
136 c = code.at(i: m_baseStart - 2);
137 if (!c.isLetterOrNumber() && c != u'_')
138 break;
139 qsizetype baseEnd = --m_baseStart;
140 while (m_baseStart != 0) {
141 QChar c = code.at(i: m_baseStart - 1);
142 if (!c.isLetterOrNumber() && c != u'_')
143 break;
144 else
145 --m_baseStart;
146 }
147 if (m_baseStart == baseEnd)
148 break;
149 }
150 m_atLineStart = true;
151 m_lineStart = m_baseStart;
152 while (m_lineStart != 0) {
153 QChar c = code.at(i: m_lineStart - 1);
154 if (c == u'\n' || c == u'\r')
155 break;
156 if (!c.isSpace())
157 m_atLineStart = false;
158 --m_lineStart;
159 }
160}
161
162enum class TypeCompletionsType { None, Types, TypesAndAttributes };
163
164enum class FunctionCompletion { None, Declaration };
165
166enum class ImportCompletionType { None, Module, Version };
167
168void CompletionRequest::sendCompletions(QmlLsp::OpenDocumentSnapshot &doc)
169{
170 QList<CompletionItem> res = completions(doc);
171 m_response.sendResponse(r: res);
172}
173
174static QList<CompletionItem> importCompletions(DomItem &file, const CompletionContextStrings &ctx)
175{
176 // returns completions for import statements, ctx is supposed to be in an import statement
177 QList<CompletionItem> res;
178 ImportCompletionType importCompletionType = ImportCompletionType::None;
179 QRegularExpression spaceRe(uR"(\W+)"_s);
180 QList<QStringView> linePieces = ctx.preLine().split(sep: spaceRe, behavior: Qt::SkipEmptyParts);
181 qsizetype effectiveLength = linePieces.size()
182 + ((!ctx.preLine().isEmpty() && ctx.preLine().last().isSpace()) ? 1 : 0);
183 if (effectiveLength < 2) {
184 CompletionItem comp;
185 comp.label = "import";
186 comp.kind = int(CompletionItemKind::Keyword);
187 res.append(t: comp);
188 }
189 if (linePieces.isEmpty() || linePieces.first() != u"import")
190 return res;
191 if (effectiveLength == 2) {
192 // the cursor is after the import, possibly in a partial module name
193 importCompletionType = ImportCompletionType::Module;
194 } else if (effectiveLength == 3) {
195 if (linePieces.last() != u"as") {
196 // the cursor is after the module, possibly in a partial version token (or partial as)
197 CompletionItem comp;
198 comp.label = "as";
199 comp.kind = int(CompletionItemKind::Keyword);
200 res.append(t: comp);
201 importCompletionType = ImportCompletionType::Version;
202 }
203 }
204 DomItem env = file.environment();
205 if (std::shared_ptr<DomEnvironment> envPtr = env.ownerAs<DomEnvironment>()) {
206 switch (importCompletionType) {
207 case ImportCompletionType::None:
208 break;
209 case ImportCompletionType::Module: {
210 QDuplicateTracker<QString> modulesSeen;
211 for (const QString &uri : envPtr->moduleIndexUris(self&: env)) {
212 QStringView base = ctx.base(); // if we allow spaces we should get rid of them
213 if (uri.startsWith(s: base)) {
214 QStringList rest = uri.mid(position: base.size()).split(sep: u'.');
215 if (rest.isEmpty())
216 continue;
217
218 const QString label = rest.first();
219 if (!modulesSeen.hasSeen(s: label)) {
220 CompletionItem comp;
221 comp.label = label.toUtf8();
222 comp.kind = int(CompletionItemKind::Module);
223 res.append(t: comp);
224 }
225 }
226 }
227 break;
228 }
229 case ImportCompletionType::Version:
230 if (ctx.base().isEmpty()) {
231 for (int majorV :
232 envPtr->moduleIndexMajorVersions(self&: env, uri: linePieces.at(i: 1).toString())) {
233 CompletionItem comp;
234 comp.label = QString::number(majorV).toUtf8();
235 comp.kind = int(CompletionItemKind::Constant);
236 res.append(t: comp);
237 }
238 } else {
239 bool hasMajorVersion = ctx.base().endsWith(c: u'.');
240 int majorV = -1;
241 if (hasMajorVersion)
242 majorV = ctx.base().mid(pos: 0, n: ctx.base().size() - 1).toInt(ok: &hasMajorVersion);
243 if (!hasMajorVersion)
244 break;
245 if (std::shared_ptr<ModuleIndex> mIndex =
246 envPtr->moduleIndexWithUri(self&: env, uri: linePieces.at(i: 1).toString(), majorVersion: majorV)) {
247 for (int minorV : mIndex->minorVersions()) {
248 CompletionItem comp;
249 comp.label = QString::number(minorV).toUtf8();
250 comp.kind = int(CompletionItemKind::Constant);
251 res.append(t: comp);
252 }
253 }
254 }
255 break;
256 }
257 }
258 return res;
259}
260
261static QList<CompletionItem> idsCompletions(DomItem component)
262{
263 qCDebug(complLog) << "adding ids completions";
264 QList<CompletionItem> res;
265 for (const QString &k : component.field(name: Fields::ids).keys()) {
266 CompletionItem comp;
267 comp.label = k.toUtf8();
268 comp.kind = int(CompletionItemKind::Value);
269 res.append(t: comp);
270 }
271 return res;
272}
273
274static QList<CompletionItem> bindingsCompletions(DomItem &containingObject)
275{
276 // returns valid bindings completions (i.e. reachable properties and signal handlers)
277 QList<CompletionItem> res;
278 qCDebug(complLog) << "binding completions";
279 containingObject.visitPrototypeChain(
280 visitor: [&res](DomItem &it) {
281 qCDebug(complLog) << "prototypeChain" << it.internalKindStr() << it.canonicalPath();
282 if (const QmlObject *itPtr = it.as<QmlObject>()) {
283 // signal handlers
284 auto methods = itPtr->methods();
285 auto it = methods.cbegin();
286 while (it != methods.cend()) {
287 if (it.value().methodType == MethodInfo::MethodType::Signal) {
288 CompletionItem comp;
289 QString signal = it.key();
290 comp.label =
291 (u"on"_s + signal.at(i: 0).toUpper() + signal.mid(position: 1)).toUtf8();
292 res.append(t: comp);
293 }
294 ++it;
295 }
296 // properties that can be bound
297 auto pDefs = itPtr->propertyDefs();
298 for (auto it2 = pDefs.keyBegin(); it2 != pDefs.keyEnd(); ++it2) {
299 qCDebug(complLog) << "adding property" << *it2;
300 CompletionItem comp;
301 comp.label = it2->toUtf8();
302 comp.insertText = (*it2 + u": "_s).toUtf8();
303 comp.kind = int(CompletionItemKind::Property);
304 res.append(t: comp);
305 }
306 }
307 return true;
308 },
309 options: VisitPrototypesOption::Normal);
310 return res;
311}
312
313static QList<CompletionItem> reachableSymbols(DomItem &context, const CompletionContextStrings &ctx,
314 TypeCompletionsType typeCompletionType,
315 FunctionCompletion completeMethodCalls)
316{
317 // returns completions for the reachable types or attributes from context
318 QList<CompletionItem> res;
319 QMap<CompletionItemKind, QSet<QString>> symbols;
320 QSet<quintptr> visited;
321 QList<Path> visitedRefs;
322 auto addLocalSymbols = [&res, typeCompletionType, completeMethodCalls, &symbols](DomItem &el) {
323 switch (typeCompletionType) {
324 case TypeCompletionsType::None:
325 return false;
326 case TypeCompletionsType::Types:
327 switch (el.internalKind()) {
328 case DomType::ImportScope: {
329 const QSet<QString> localSymbols = el.localSymbolNames(
330 lTypes: LocalSymbolsType::QmlTypes | LocalSymbolsType::Namespaces);
331 qCDebug(complLog) << "adding local symbols of:" << el.internalKindStr()
332 << el.canonicalPath() << localSymbols;
333 symbols[CompletionItemKind::Class] += localSymbols;
334 break;
335 }
336 default: {
337 qCDebug(complLog) << "skipping local symbols for non type" << el.internalKindStr()
338 << el.canonicalPath();
339 break;
340 }
341 }
342 break;
343 case TypeCompletionsType::TypesAndAttributes:
344 auto localSymbols = el.localSymbolNames(lTypes: LocalSymbolsType::All);
345 if (const QmlObject *elPtr = el.as<QmlObject>()) {
346 auto methods = elPtr->methods();
347 auto it = methods.cbegin();
348 while (it != methods.cend()) {
349 localSymbols.remove(value: it.key());
350 if (completeMethodCalls == FunctionCompletion::Declaration) {
351 QStringList parameters;
352 for (const MethodParameter &pInfo : std::as_const(t: it->parameters)) {
353 QStringList param;
354 if (!pInfo.typeName.isEmpty())
355 param << pInfo.typeName;
356 if (!pInfo.name.isEmpty())
357 param << pInfo.name;
358 if (pInfo.defaultValue) {
359 param << u"= " + pInfo.defaultValue->code();
360 }
361 parameters.append(t: param.join(sep: u' '));
362 }
363
364 QString commentsStr;
365
366 if (!it->comments.regionComments.isEmpty()) {
367 for (const Comment &c : it->comments.regionComments[QString()].preComments) {
368 commentsStr += c.rawComment().toString().trimmed() + u'\n';
369 }
370 }
371
372 CompletionItem comp;
373 comp.documentation =
374 u"%1%2(%3)"_s.arg(args&: commentsStr, args: it.key(), args: parameters.join(sep: u", "))
375 .toUtf8();
376 comp.label = (it.key() + u"()").toUtf8();
377 comp.kind = int(CompletionItemKind::Function);
378
379 if (it->typeName.isEmpty())
380 comp.detail = "returns void";
381 else
382 comp.detail = (u"returns "_s + it->typeName).toUtf8();
383
384 // Only append full bracket if there are no parameters
385 if (it->parameters.isEmpty())
386 comp.insertText = comp.label;
387 else
388 // add snippet support?
389 comp.insertText = (it.key() + u"(").toUtf8();
390
391 res.append(t: comp);
392 }
393 ++it;
394 }
395 }
396 qCDebug(complLog) << "adding local symbols of:" << el.internalKindStr()
397 << el.canonicalPath() << localSymbols;
398 symbols[CompletionItemKind::Field] += localSymbols;
399 break;
400 }
401 return true;
402 };
403 if (ctx.base().isEmpty()) {
404 if (typeCompletionType != TypeCompletionsType::None) {
405 qCDebug(complLog) << "adding symbols reachable from:" << context.internalKindStr()
406 << context.canonicalPath();
407 DomItem it = context.proceedToScope();
408 it.visitScopeChain(visitor: addLocalSymbols, LookupOption::Normal, h: &defaultErrorHandler,
409 visited: &visited, visitedRefs: &visitedRefs);
410 }
411 } else {
412 QList<QStringView> baseItems = ctx.base().split(sep: u'.', behavior: Qt::SkipEmptyParts);
413 Q_ASSERT(!baseItems.isEmpty());
414 auto addReachableSymbols = [&visited, &visitedRefs, &addLocalSymbols](Path,
415 DomItem &it) -> bool {
416 qCDebug(complLog) << "adding directly accessible symbols of" << it.internalKindStr()
417 << it.canonicalPath();
418 it.visitDirectAccessibleScopes(visitor: addLocalSymbols, options: VisitPrototypesOption::Normal,
419 h: &defaultErrorHandler, visited: &visited, visitedRefs: &visitedRefs);
420 return true;
421 };
422 Path toSearch = Paths::lookupSymbolPath(name: ctx.base().toString().chopped(n: 1));
423 context.resolve(path: toSearch, visitor: addReachableSymbols, errorHandler: &defaultErrorHandler);
424 // add attached types? technically we should...
425 }
426 for (auto symbolKinds = symbols.constBegin(); symbolKinds != symbols.constEnd();
427 ++symbolKinds) {
428 for (auto symbol = symbolKinds.value().constBegin();
429 symbol != symbolKinds.value().constEnd(); ++symbol) {
430 CompletionItem comp;
431 comp.label = symbol->toUtf8();
432 comp.kind = int(symbolKinds.key());
433 res.append(t: comp);
434 }
435 }
436 return res;
437}
438
439QList<CompletionItem> CompletionRequest::completions(QmlLsp::OpenDocumentSnapshot &doc) const
440{
441 QList<CompletionItem> res;
442 if (!doc.validDoc) {
443 qCWarning(complLog) << "No valid document for completions for "
444 << QString::fromUtf8(ba: m_parameters.textDocument.uri);
445 // try to add some import and global completions?
446 return res;
447 }
448 if (!doc.docVersion || *doc.docVersion < m_minVersion) {
449 qCWarning(complLog) << "sendCompletions on older doc version";
450 } else if (!doc.validDocVersion || *doc.validDocVersion < m_minVersion) {
451 qCWarning(complLog) << "using outdated valid doc, position might be incorrect";
452 }
453 DomItem file = doc.validDoc.fileObject(option: QQmlJS::Dom::GoTo::MostLikely);
454 // clear reference cache to resolve latest versions (use a local env instead?)
455 if (std::shared_ptr<DomEnvironment> envPtr = file.environment().ownerAs<DomEnvironment>())
456 envPtr->clearReferenceCache();
457 qsizetype pos = QQmlLSUtils::textOffsetFrom(code, row: m_parameters.position.line,
458 character: m_parameters.position.character);
459 CompletionContextStrings ctx(code, pos);
460 auto itemsFound = QQmlLSUtils::itemsFromTextLocation(file, line: m_parameters.position.line,
461 character: m_parameters.position.character
462 - ctx.filterChars().size());
463 if (itemsFound.size() > 1) {
464 QStringList paths;
465 for (auto &it : itemsFound)
466 paths.append(t: it.domItem.canonicalPath().toString());
467 qCWarning(complLog) << "Multiple elements of " << urlAndPos()
468 << " at the same depth:" << paths << "(using first)";
469 }
470 DomItem currentItem;
471 if (!itemsFound.isEmpty())
472 currentItem = itemsFound.first().domItem;
473 qCDebug(complLog) << "Completion at " << urlAndPos() << " " << m_parameters.position.line << ":"
474 << m_parameters.position.character << "offset:" << pos
475 << "base:" << ctx.base() << "filter:" << ctx.filterChars()
476 << "lastVersion:" << (doc.docVersion ? (*doc.docVersion) : -1)
477 << "validVersion:" << (doc.validDocVersion ? (*doc.validDocVersion) : -1)
478 << "in" << currentItem.internalKindStr() << currentItem.canonicalPath();
479 DomItem containingObject = currentItem.qmlObject();
480 TypeCompletionsType typeCompletionType = TypeCompletionsType::None;
481 FunctionCompletion methodCompletion = FunctionCompletion::Declaration;
482
483 if (!containingObject) {
484 methodCompletion = FunctionCompletion::None;
485 // global completions
486 if (ctx.atLineStart()) {
487 if (ctx.base().isEmpty()) {
488 {
489 CompletionItem comp;
490 comp.label = "pragma";
491 comp.kind = int(CompletionItemKind::Keyword);
492 res.append(t: comp);
493 }
494 }
495 typeCompletionType = TypeCompletionsType::Types;
496 }
497 // Import completion
498 res += importCompletions(file, ctx);
499 } else {
500 methodCompletion = FunctionCompletion::Declaration;
501 bool addIds = false;
502
503 if (ctx.atLineStart() && currentItem.internalKind() != DomType::ScriptExpression
504 && currentItem.internalKind() != DomType::List) {
505 // add bindings
506 methodCompletion = FunctionCompletion::None;
507 if (ctx.base().isEmpty()) {
508 for (const QStringView &s : std::array<QStringView, 5>(
509 { u"property", u"readonly", u"default", u"signal", u"function" })) {
510 CompletionItem comp;
511 comp.label = s.toUtf8();
512 comp.kind = int(CompletionItemKind::Keyword);
513 res.append(t: comp);
514 }
515 res += bindingsCompletions(containingObject);
516 typeCompletionType = TypeCompletionsType::Types;
517 } else {
518 // handle value types later with type expansion
519 typeCompletionType = TypeCompletionsType::TypesAndAttributes;
520 }
521 } else {
522 addIds = true;
523 typeCompletionType = TypeCompletionsType::TypesAndAttributes;
524 }
525 if (addIds) {
526 res += idsCompletions(component: containingObject.component());
527 }
528 }
529
530 DomItem context = containingObject;
531 if (!context)
532 context = file;
533 // adds types and attributes
534 res += reachableSymbols(context, ctx, typeCompletionType, completeMethodCalls: methodCompletion);
535 return res;
536}
537QT_END_NAMESPACE
538

source code of qtdeclarative/src/qmlls/qqmlcompletionsupport.cpp