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 | |
14 | QT_BEGIN_NAMESPACE |
15 | using namespace QLspSpecification; |
16 | using namespace QQmlJS::Dom; |
17 | using namespace Qt::StringLiterals; |
18 | |
19 | Q_LOGGING_CATEGORY(complLog, "qt.languageserver.completions" ) |
20 | |
21 | bool CompletionRequest::fillFrom(QmlLsp::OpenDocument doc, const Parameters ¶ms, |
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 | |
43 | void 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 | |
51 | QString QmlCompletionSupport::name() const |
52 | { |
53 | return u"QmlCompletionSupport"_s ; |
54 | } |
55 | |
56 | void 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 | |
68 | void 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 | |
75 | QString 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 |
84 | struct CompletionContextStrings |
85 | { |
86 | CompletionContextStrings(QString code, qsizetype pos); |
87 | |
88 | public: |
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 | |
107 | private: |
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 | |
116 | CompletionContextStrings::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 | |
162 | enum class TypeCompletionsType { None, Types, TypesAndAttributes }; |
163 | |
164 | enum class FunctionCompletion { None, Declaration }; |
165 | |
166 | enum class ImportCompletionType { None, Module, Version }; |
167 | |
168 | void CompletionRequest::sendCompletions(QmlLsp::OpenDocumentSnapshot &doc) |
169 | { |
170 | QList<CompletionItem> res = completions(doc); |
171 | m_response.sendResponse(r: res); |
172 | } |
173 | |
174 | static 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 | |
261 | static 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 | |
274 | static 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 | |
313 | static 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 ; |
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 | |
439 | QList<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 | } |
537 | QT_END_NAMESPACE |
538 | |