aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorFawzi Mohamed <fawzi.mohamed@qt.io>2022-03-14 14:51:13 +0100
committerFawzi Mohamed <fawzi.mohamed@qt.io>2022-05-19 11:03:39 +0200
commit538600cf5adb748f7162aa8ea706072a7874bd02 (patch)
treee0866d9de4166d3c4e7546c12a367809e146741d /tools
parent2717985373dc3e2765ced402c775a8b4386b1e93 (diff)
qmlls: completion support
Add support for completions to qmlls. This is a preliminary support that uses QmlDom. For this reason it has some limitations, mainly that it only uses QML types, no js completions are provided, and no type propagation is kept into account. On the plus side it is immediately up to date when dependencies are updated. The completion tries to find in which part of the qml file one is and distinguishes mainly 4 parts: * outside all QmlObjects * in an import * in a QmlObject, declaring a new binding * in a script, or similar It looks at the position in the editor and ts context, namely: * the word before the cursor (which is used as filter in the client), * the dot expression before that word * the whole line before the cursor. Change-Id: Iefd1332451c060d3d10c68bb427b26c1bc020666 Reviewed-by: Sona Kurazyan <sona.kurazyan@qt.io> Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
Diffstat (limited to 'tools')
-rw-r--r--tools/qmlls/CMakeLists.txt1
-rw-r--r--tools/qmlls/qmlcompletionsupport.cpp655
-rw-r--r--tools/qmlls/qmlcompletionsupport.h72
-rw-r--r--tools/qmlls/qmllanguageservertool.cpp1
-rw-r--r--tools/qmlls/qqmlcodemodel.cpp1
-rw-r--r--tools/qmlls/qqmllanguageserver.cpp4
-rw-r--r--tools/qmlls/qqmllanguageserver.h2
7 files changed, 735 insertions, 1 deletions
diff --git a/tools/qmlls/CMakeLists.txt b/tools/qmlls/CMakeLists.txt
index 57c8e28d0e..3153e0d00a 100644
--- a/tools/qmlls/CMakeLists.txt
+++ b/tools/qmlls/CMakeLists.txt
@@ -17,6 +17,7 @@ qt_internal_add_tool(${target_name}
textdocument.cpp textdocument.h
qmllintsuggestions.h qmllintsuggestions.cpp
textsynchronization.cpp textsynchronization.h
+ qmlcompletionsupport.h qmlcompletionsupport.cpp
qqmlcodemodel.h qqmlcodemodel.cpp
../shared/qqmltoolingsettings.h
../shared/qqmltoolingsettings.cpp
diff --git a/tools/qmlls/qmlcompletionsupport.cpp b/tools/qmlls/qmlcompletionsupport.cpp
new file mode 100644
index 0000000000..f731a9b67a
--- /dev/null
+++ b/tools/qmlls/qmlcompletionsupport.cpp
@@ -0,0 +1,655 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "qmlcompletionsupport.h"
+#include "qqmllanguageserver.h"
+#include <QtLanguageServer/private/qlanguageserverspectypes_p.h>
+#include <QtCore/qthreadpool.h>
+#include <QtCore/QRegularExpression>
+#include <QtQmlDom/private/qqmldomexternalitems_p.h>
+#include <QtQmlDom/private/qqmldomtop_p.h>
+
+QT_USE_NAMESPACE
+using namespace QLspSpecification;
+using namespace QQmlJS::Dom;
+using namespace Qt::StringLiterals;
+
+Q_LOGGING_CATEGORY(complLog, "qt.languageserver.completions")
+
+void QmlCompletionSupport::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol)
+{
+ protocol->registerCompletionRequestHandler(
+ [this](const QByteArray &, const CompletionParams &cParams,
+ LSPPartialResponse<
+ std::variant<QList<CompletionItem>, CompletionList, std::nullptr_t>,
+ std::variant<CompletionList, QList<CompletionItem>>> &&response) {
+ QmlLsp::OpenDocument doc = m_codeModel->openDocumentByUrl(
+ QmlLsp::lspUriToQmlUrl(cParams.textDocument.uri));
+ if (!doc.textDocument) {
+ response.sendResponse(QList<CompletionItem>());
+ return;
+ }
+ CompletionRequest *req = new CompletionRequest;
+ std::optional<int> targetVersion;
+ {
+ QMutexLocker l(doc.textDocument->mutex());
+ targetVersion = doc.textDocument->version();
+ if (!targetVersion) {
+ qCWarning(complLog) << "no target version for completions in "
+ << QString::fromUtf8(cParams.textDocument.uri);
+ }
+ req->code = doc.textDocument->toPlainText();
+ }
+ req->minVersion = (targetVersion ? *targetVersion : 0);
+ req->response = std::move(response);
+ req->completionParams = cParams;
+ {
+ QMutexLocker l(&m_mutex);
+ m_completions.insert(req->completionParams.textDocument.uri, req);
+ }
+ if (doc.snapshot.docVersion && *doc.snapshot.docVersion >= req->minVersion)
+ updatedSnapshot(QmlLsp::lspUriToQmlUrl(req->completionParams.textDocument.uri));
+ });
+ protocol->registerCompletionItemResolveRequestHandler(
+ [](const QByteArray &, const CompletionItem &cParams,
+ LSPResponse<CompletionItem> &&response) { response.sendResponse(cParams); });
+}
+
+QmlCompletionSupport::QmlCompletionSupport(QmlLsp::QQmlCodeModel *codeModel)
+ : m_codeModel(codeModel)
+{
+ QObject::connect(m_codeModel, &QmlLsp::QQmlCodeModel::updatedSnapshot, this,
+ &QmlCompletionSupport::updatedSnapshot);
+}
+
+QmlCompletionSupport::~QmlCompletionSupport()
+{
+ QMutexLocker l(&m_mutex);
+ qDeleteAll(m_completions);
+ m_completions.clear();
+}
+
+QString QmlCompletionSupport::name() const
+{
+ return u"QmlCompletionSupport"_s;
+}
+
+void QmlCompletionSupport::setupCapabilities(
+ const QLspSpecification::InitializeParams &,
+ QLspSpecification::InitializeResult &serverCapabilities)
+{
+ QLspSpecification::CompletionOptions cOptions;
+ if (serverCapabilities.capabilities.completionProvider)
+ cOptions = *serverCapabilities.capabilities.completionProvider;
+ cOptions.resolveProvider = true;
+ cOptions.triggerCharacters = QList<QByteArray>({ QByteArray(".") });
+ serverCapabilities.capabilities.completionProvider = cOptions;
+}
+
+void QmlCompletionSupport::updatedSnapshot(const QByteArray &url)
+{
+ QmlLsp::OpenDocumentSnapshot doc = m_codeModel->snapshotByUrl(url);
+ QList<CompletionRequest *> toCompl;
+ {
+ QMutexLocker l(&m_mutex);
+ for (auto [it, end] = m_completions.equal_range(url); it != end; ++it) {
+ if (doc.docVersion && it.value()->minVersion <= *doc.docVersion)
+ toCompl.append(it.value());
+ }
+ if (!m_completions.isEmpty())
+ qCDebug(complLog) << "updated " << QString::fromUtf8(url) << " v"
+ << (doc.docVersion ? (*doc.docVersion) : -1) << ", completing"
+ << m_completions.size() << "/" << m_completions.size();
+ for (auto req : toCompl)
+ m_completions.remove(url, req);
+ }
+ for (auto it = toCompl.rbegin(), end = toCompl.rend(); it != end; ++it) {
+ CompletionRequest *req = *it;
+ QThreadPool::globalInstance()->start([req, doc]() mutable {
+ req->sendCompletions(doc);
+ delete req;
+ });
+ }
+}
+
+struct ItemLocation
+{
+ int depth = -1;
+ DomItem domItem;
+ FileLocations::Tree fileLocation;
+};
+
+QString CompletionRequest::urlAndPos() const
+{
+ return QString::fromUtf8(completionParams.textDocument.uri) + u":"
+ + QString::number(completionParams.position.line) + u":"
+ + QString::number(completionParams.position.character);
+}
+
+// return the position of 0 based line and char offsets, never goes to the "next" line, but might
+// return the position of the \n or \r that starts the next line (never the one that starts line)
+static qsizetype posAfterLineChar(QString code, int line, int character)
+{
+ int targetLine = line;
+ qsizetype i = 0;
+ while (i != code.length() && targetLine != 0) {
+ QChar c = code.at(i++);
+ if (c == u'\n') {
+ --targetLine;
+ }
+ if (c == u'\r') {
+ if (i != code.length() && code.at(i) == u'\n')
+ ++i;
+ --targetLine;
+ }
+ }
+ qsizetype leftChars = character;
+ while (i != code.length() && leftChars) {
+ QChar c = code.at(i);
+ if (c == u'\n' || c == u'\r')
+ break;
+ ++i;
+ if (!c.isLowSurrogate())
+ --leftChars;
+ }
+ return i;
+}
+
+static QList<ItemLocation> findLastItemsContaining(DomItem file, int line, int character)
+{
+ QList<ItemLocation> itemsFound;
+ std::shared_ptr<QmlFile> filePtr = file.ownerAs<QmlFile>();
+ if (!filePtr)
+ return itemsFound;
+ FileLocations::Tree t = filePtr->fileLocationsTree();
+ Q_ASSERT(t);
+ QString code = filePtr->code(); // do something more advanced wrt to changes wrt to this->code?
+ if (code.isEmpty())
+ qCWarning(complLog) << "no code";
+ QList<ItemLocation> toDo;
+ qsizetype targetPos = posAfterLineChar(code, line, character);
+ auto containsTarget = [targetPos](QQmlJS::SourceLocation l) {
+ return l.begin() <= targetPos && targetPos < l.end();
+ };
+ if (containsTarget(t->info().fullRegion)) {
+ ItemLocation loc;
+ loc.depth = 0;
+ loc.domItem = file;
+ loc.fileLocation = t;
+ toDo.append(loc);
+ }
+ while (!toDo.isEmpty()) {
+ ItemLocation iLoc = toDo.last();
+ toDo.removeLast();
+ if (itemsFound.isEmpty() || itemsFound.constFirst().depth <= iLoc.depth) {
+ if (!itemsFound.isEmpty() && itemsFound.constFirst().depth < iLoc.depth)
+ itemsFound.clear();
+ itemsFound.append(iLoc);
+ }
+ auto subEls = iLoc.fileLocation->subItems();
+ for (auto it = subEls.begin(); it != subEls.end(); ++it) {
+ auto subLoc = std::static_pointer_cast<AttachedInfoT<FileLocations>>(it.value());
+ Q_ASSERT(subLoc);
+ if (containsTarget(subLoc->info().fullRegion)) {
+ ItemLocation subItem;
+ subItem.depth = iLoc.depth + 1;
+ subItem.domItem = iLoc.domItem.path(it.key());
+ subItem.fileLocation = subLoc;
+ toDo.append(subItem);
+ }
+ }
+ }
+ return itemsFound;
+}
+
+// finds the filter string, the base (for fully qualified accesses) and the whole string
+// just before pos in code
+struct CompletionContextStrings
+{
+ CompletionContextStrings(QString code, qsizetype pos);
+
+public:
+ // line up until pos
+ QStringView preLine() const
+ {
+ return QStringView(m_code).mid(m_lineStart, m_pos - m_lineStart);
+ }
+ // the part used to filter the completion (normally actual filtering is left to the client)
+ QStringView filterChars() const
+ {
+ return QStringView(m_code).mid(m_filterStart, m_pos - m_filterStart);
+ }
+ // the base part (qualified access)
+ QStringView base() const
+ {
+ return QStringView(m_code).mid(m_baseStart, m_filterStart - m_baseStart);
+ }
+ // if we are at line start
+ bool atLineStart() const { return m_atLineStart; }
+
+private:
+ QString m_code; // the current code
+ qsizetype m_pos = {}; // current position of the cursor
+ qsizetype m_filterStart = {}; // start of the characters that are used to filter the suggestions
+ qsizetype m_lineStart = {}; // start of the current line
+ qsizetype m_baseStart = {}; // start of the dotted expression that ends at the cursor position
+ bool m_atLineStart = {}; // if there are only spaces before base
+};
+
+CompletionContextStrings::CompletionContextStrings(QString code, qsizetype pos)
+ : m_code(code), m_pos(pos)
+{
+ // computes the context just before pos in code.
+ // After this code all the values of all the attributes should be correct (see above)
+ // handle also letter or numbers represented a surrogate pairs?
+ m_filterStart = m_pos;
+ while (m_filterStart != 0) {
+ QChar c = code.at(m_filterStart - 1);
+ if (!c.isLetterOrNumber() && c != u'_')
+ break;
+ else
+ --m_filterStart;
+ }
+ // handle spaces?
+ m_baseStart = m_filterStart;
+ while (m_baseStart != 0) {
+ QChar c = code.at(m_baseStart - 1);
+ if (c != u'.' || m_baseStart == 1)
+ break;
+ c = code.at(m_baseStart - 2);
+ if (!c.isLetterOrNumber() && c != u'_')
+ break;
+ qsizetype baseEnd = --m_baseStart;
+ while (m_baseStart != 0) {
+ QChar c = code.at(m_baseStart - 1);
+ if (!c.isLetterOrNumber() && c != u'_')
+ break;
+ else
+ --m_baseStart;
+ }
+ if (m_baseStart == baseEnd)
+ break;
+ }
+ m_atLineStart = true;
+ m_lineStart = m_baseStart;
+ while (m_lineStart != 0) {
+ QChar c = code.at(m_lineStart - 1);
+ if (c == u'\n' || c == '\r')
+ break;
+ if (!c.isSpace())
+ m_atLineStart = false;
+ --m_lineStart;
+ }
+}
+
+enum class TypeCompletionsType { None, Types, TypesAndAttributes };
+
+enum class FunctionCompletion { None, Declaration };
+
+enum class ImportCompletionType { None, Module, Version };
+
+void CompletionRequest::sendCompletions(QmlLsp::OpenDocumentSnapshot &doc)
+{
+ QList<CompletionItem> res = completions(doc);
+ response.sendResponse(res);
+}
+
+static QList<CompletionItem> importCompletions(DomItem &file, const CompletionContextStrings &ctx)
+{
+ // returns completions for import statements, ctx is supposed to be in an import statement
+ QList<CompletionItem> res;
+ ImportCompletionType importCompletionType = ImportCompletionType::None;
+ QRegularExpression spaceRe(uR"(\W+)"_s);
+ QList<QStringView> linePieces = ctx.preLine().split(spaceRe, Qt::SkipEmptyParts);
+ qsizetype effectiveLength = linePieces.length()
+ + ((!ctx.preLine().isEmpty() && ctx.preLine().last().isSpace()) ? 1 : 0);
+ if (effectiveLength < 2) {
+ CompletionItem comp;
+ comp.label = "import";
+ comp.kind = int(CompletionItemKind::Keyword);
+ res.append(comp);
+ }
+ if (linePieces.isEmpty() || linePieces.first() != u"import")
+ return res;
+ if (effectiveLength == 2) {
+ // the cursor is after the import, possibly in a partial module name
+ importCompletionType = ImportCompletionType::Module;
+ } else if (effectiveLength == 3) {
+ if (linePieces.last() != u"as") {
+ // the cursor is after the module, possibly in a partial version token (or partial as)
+ CompletionItem comp;
+ comp.label = "as";
+ comp.kind = int(CompletionItemKind::Keyword);
+ res.append(comp);
+ importCompletionType = ImportCompletionType::Version;
+ }
+ }
+ DomItem env = file.environment();
+ if (std::shared_ptr<DomEnvironment> envPtr = env.ownerAs<DomEnvironment>()) {
+ switch (importCompletionType) {
+ case ImportCompletionType::None:
+ break;
+ case ImportCompletionType::Module:
+ for (const QString &uri : envPtr->moduleIndexUris(env)) {
+ QStringView base = ctx.base(); // if we allow spaces we should get rid of them
+ if (uri.startsWith(base)) {
+ QStringList rest = uri.mid(base.length()).split(u'.');
+ if (rest.isEmpty())
+ continue;
+ CompletionItem comp;
+ comp.label = rest.first().toUtf8();
+ comp.kind = int(CompletionItemKind::Module);
+ res.append(comp);
+ }
+ }
+ break;
+ case ImportCompletionType::Version:
+ if (ctx.base().isEmpty()) {
+ for (int majorV :
+ envPtr->moduleIndexMajorVersions(env, linePieces.at(1).toString())) {
+ CompletionItem comp;
+ comp.label = QString::number(majorV).toUtf8();
+ comp.kind = int(CompletionItemKind::Constant);
+ res.append(comp);
+ }
+ } else {
+ bool hasMajorVersion = ctx.base().endsWith(u'.');
+ int majorV = -1;
+ if (hasMajorVersion)
+ majorV = ctx.base().mid(0, ctx.base().length() - 1).toInt(&hasMajorVersion);
+ if (!hasMajorVersion)
+ break;
+ if (std::shared_ptr<ModuleIndex> mIndex =
+ envPtr->moduleIndexWithUri(env, linePieces.at(1).toString(), majorV)) {
+ for (int minorV : mIndex->minorVersions()) {
+ CompletionItem comp;
+ comp.label = QString::number(minorV).toUtf8();
+ comp.kind = int(CompletionItemKind::Constant);
+ res.append(comp);
+ }
+ }
+ }
+ break;
+ }
+ }
+ return res;
+}
+
+static QList<CompletionItem> idsCompletions(DomItem component)
+{
+ qCDebug(complLog) << "adding ids completions";
+ QList<CompletionItem> res;
+ for (const QString &k : component.field(Fields::ids).keys()) {
+ CompletionItem comp;
+ comp.label = k.toUtf8();
+ comp.kind = int(CompletionItemKind::Value);
+ res.append(comp);
+ }
+ return res;
+}
+
+static QList<CompletionItem> bindingsCompletions(DomItem &containingObject)
+{
+ // returns valid bindings completions (i.e. reachable properties and signal handlers)
+ QList<CompletionItem> res;
+ qCDebug(complLog) << "binding completions";
+ containingObject.visitPrototypeChain(
+ [&res](DomItem &it) {
+ qCDebug(complLog) << "prototypeChain" << it.internalKindStr() << it.canonicalPath();
+ if (const QmlObject *itPtr = it.as<QmlObject>()) {
+ // signal handlers
+ auto methods = itPtr->methods();
+ auto it = methods.cbegin();
+ while (it != methods.cend()) {
+ if (it.value().methodType == MethodInfo::MethodType::Signal) {
+ CompletionItem comp;
+ QString signal = it.key();
+ comp.label =
+ (u"on"_s + signal.at(0).toUpper() + signal.mid(1)).toUtf8();
+ res.append(comp);
+ }
+ ++it;
+ }
+ // properties that can be bound
+ auto pDefs = itPtr->propertyDefs();
+ for (auto it2 = pDefs.keyBegin(); it2 != pDefs.keyEnd(); ++it2) {
+ qCDebug(complLog) << "adding property" << *it2;
+ CompletionItem comp;
+ comp.label = it2->toUtf8();
+ comp.kind = int(CompletionItemKind::Field);
+ res.append(comp);
+ }
+ }
+ return true;
+ },
+ VisitPrototypesOption::Normal);
+ return res;
+}
+
+static QList<CompletionItem> reachableSymbols(DomItem &context, const CompletionContextStrings &ctx,
+ TypeCompletionsType typeCompletionType,
+ FunctionCompletion completeMethodCalls)
+{
+ // returns completions for the reachable types or attributes from context
+ QList<CompletionItem> res;
+ QSet<QString> symbols;
+ QSet<quintptr> visited;
+ QList<Path> visitedRefs;
+ auto addReachableSymbols = [&res, &symbols, &visited, &visitedRefs, typeCompletionType,
+ completeMethodCalls](Path, DomItem &it) -> bool {
+ qCDebug(complLog) << "adding symbols reachable from:" << it.internalKindStr()
+ << it.canonicalPath();
+ it = it.proceedToScope();
+ it.visitScopeChain(
+ [&res, typeCompletionType, completeMethodCalls, &symbols](DomItem &el) {
+ switch (typeCompletionType) {
+ case TypeCompletionsType::None:
+ return false;
+ case TypeCompletionsType::Types:
+ switch (el.internalKind()) {
+ case DomType::ImportScope:
+ qCDebug(complLog)
+ << "scope lookup, adding local symbols of:"
+ << el.internalKindStr() << el.canonicalPath()
+ << el.localSymbolNames(LocalSymbolsType::QmlTypes
+ | LocalSymbolsType::Namespaces);
+ symbols += el.localSymbolNames(LocalSymbolsType::QmlTypes
+ | LocalSymbolsType::Namespaces);
+ break;
+ default:
+ qCDebug(complLog) << "scope lookup, skipping non type"
+ << el.internalKindStr() << el.canonicalPath();
+ break;
+ }
+ break;
+ case TypeCompletionsType::TypesAndAttributes:
+ auto localSymbols = el.localSymbolNames(LocalSymbolsType::All);
+ if (const QmlObject *elPtr = el.as<QmlObject>()) {
+ auto methods = elPtr->methods();
+ auto it = methods.cbegin();
+ while (it != methods.cend()) {
+ localSymbols.remove(it.key());
+ if (completeMethodCalls == FunctionCompletion::Declaration) {
+ CompletionItem comp;
+ QString label = it.key() + u"(";
+ QString doc = it.key() + u"(";
+ bool first = true;
+ for (const MethodParameter &pInfo : qAsConst(it->parameters)) {
+ if (first)
+ first = false;
+ else {
+ label += u",";
+ doc += u",";
+ }
+ label += pInfo.name;
+ if (!pInfo.typeName.isEmpty())
+ doc += pInfo.typeName + u" ";
+ doc += pInfo.name;
+ if (pInfo.defaultValue) {
+ doc += u"=";
+ doc += pInfo.defaultValue->code();
+ }
+ }
+ comp.detail = label.toUtf8();
+ comp.label = (it.key() + u"()").toUtf8();
+ comp.documentation = doc.toUtf8();
+ res.append(comp);
+ }
+ ++it;
+ }
+ }
+ qCDebug(complLog)
+ << "scope lookup, adding local symbols of:" << el.internalKindStr()
+ << el.canonicalPath() << localSymbols;
+ symbols += localSymbols;
+ break;
+ }
+ return true;
+ },
+ LookupOption::Normal, &defaultErrorHandler, &visited, &visitedRefs);
+ return true;
+ };
+ if (ctx.base().isEmpty()) {
+ if (typeCompletionType != TypeCompletionsType::None) {
+ DomItem importScope = context.fileObject().field(Fields::importScope);
+ qCDebug(complLog) << "adding modules and direct accessible types in"
+ << importScope.internalKindStr() << importScope.canonicalPath();
+ addReachableSymbols(Path(), context);
+ }
+ } else {
+ QList<QStringView> baseItems = ctx.base().split(u'.');
+ Q_ASSERT(!baseItems.isEmpty());
+ Path toSearch = Paths::lookupSymbolPath(ctx.base().toString().chopped(1));
+ context.resolve(toSearch, addReachableSymbols, &defaultErrorHandler);
+ }
+ for (const QString &s : qAsConst(symbols)) {
+ CompletionItem comp;
+ comp.label = s.toUtf8();
+ comp.kind = int(CompletionItemKind::Field);
+ res.append(comp);
+ }
+ return res;
+}
+
+QList<CompletionItem> CompletionRequest::completions(QmlLsp::OpenDocumentSnapshot &doc) const
+{
+ QList<CompletionItem> res;
+ if (!doc.validDoc) {
+ qCWarning(complLog) << "No valid document for completions for "
+ << QString::fromUtf8(completionParams.textDocument.uri);
+ // try to add some import and global completions?
+ return res;
+ }
+ if (!doc.docVersion || *doc.docVersion < minVersion) {
+ qCWarning(complLog) << "sendCompletions on older doc version";
+ } else if (!doc.validDocVersion || *doc.validDocVersion < minVersion) {
+ qCWarning(complLog) << "using outdated valid doc, position might be incorrect";
+ }
+ DomItem file = doc.validDoc.fileObject(QQmlJS::Dom::GoTo::MostLikely);
+ // clear reference cache to resolve latest versions (use a local env instead?)
+ if (std::shared_ptr<DomEnvironment> envPtr = file.environment().ownerAs<DomEnvironment>())
+ envPtr->clearReferenceCache();
+ qsizetype pos = posAfterLineChar(code, completionParams.position.line,
+ completionParams.position.character);
+ CompletionContextStrings ctx(code, pos);
+ QList<ItemLocation> itemsFound =
+ findLastItemsContaining(file, completionParams.position.line,
+ completionParams.position.character - ctx.filterChars().size());
+ if (itemsFound.length() > 1) {
+ QStringList paths;
+ for (auto &it : itemsFound)
+ paths.append(it.domItem.canonicalPath().toString());
+ qCWarning(complLog) << "Multiple elements of " << urlAndPos()
+ << " at the same depth:" << paths << "(using first)";
+ }
+ DomItem currentItem;
+ if (!itemsFound.isEmpty())
+ currentItem = itemsFound.first().domItem;
+ qCDebug(complLog) << "Completion at " << urlAndPos() << " " << completionParams.position.line
+ << ":" << completionParams.position.character << "offset:" << pos
+ << "lastVersion:" << (doc.docVersion ? (*doc.docVersion) : -1)
+ << "validVersion:" << (doc.validDocVersion ? (*doc.validDocVersion) : -1)
+ << "in" << currentItem.internalKindStr() << currentItem.canonicalPath();
+ DomItem containingObject = currentItem.qmlObject();
+ TypeCompletionsType typeCompletionType = TypeCompletionsType::None;
+ FunctionCompletion methodCompletion = FunctionCompletion::Declaration;
+
+ if (!containingObject) {
+ methodCompletion = FunctionCompletion::None;
+ // global completions
+ if (ctx.atLineStart()) {
+ if (ctx.base().isEmpty()) {
+ {
+ CompletionItem comp;
+ comp.label = "pragma";
+ comp.kind = int(CompletionItemKind::Keyword);
+ res.append(comp);
+ }
+ }
+ typeCompletionType = TypeCompletionsType::Types;
+ }
+ // Import completion
+ res += importCompletions(file, ctx);
+ } else {
+ methodCompletion = FunctionCompletion::Declaration;
+ bool addIds = false;
+
+ if (ctx.atLineStart() && currentItem.internalKind() != DomType::ScriptExpression
+ && currentItem.internalKind() != DomType::List) {
+ // add bindings
+ methodCompletion = FunctionCompletion::None;
+ if (ctx.base().isEmpty()) {
+ for (const QStringView &s : std::array<QStringView, 5>(
+ { u"property", u"readonly", u"default", u"signal", u"function" })) {
+ CompletionItem comp;
+ comp.label = s.toUtf8();
+ comp.kind = int(CompletionItemKind::Keyword);
+ res.append(comp);
+ }
+ res += bindingsCompletions(containingObject);
+ typeCompletionType = TypeCompletionsType::Types;
+ } else {
+ // handle value types later with type expansion
+ typeCompletionType = TypeCompletionsType::TypesAndAttributes;
+ }
+ } else {
+ addIds = true;
+ typeCompletionType = TypeCompletionsType::TypesAndAttributes;
+ }
+ if (addIds) {
+ res += idsCompletions(containingObject.component());
+ }
+ }
+
+ DomItem context = containingObject;
+ if (!context)
+ context = file;
+ // adds types and attributes
+ res += reachableSymbols(context, ctx, typeCompletionType, methodCompletion);
+ return res;
+}
diff --git a/tools/qmlls/qmlcompletionsupport.h b/tools/qmlls/qmlcompletionsupport.h
new file mode 100644
index 0000000000..79aac2aa5b
--- /dev/null
+++ b/tools/qmlls/qmlcompletionsupport.h
@@ -0,0 +1,72 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QMLCOMPLETIONSUPPORT_H
+#define QMLCOMPLETIONSUPPORT_H
+
+#include "qlanguageserver.h"
+#include "qqmlcodemodel.h"
+#include <QtCore/qmutex.h>
+#include <QtCore/qhash.h>
+
+struct CompletionRequest
+{
+ int minVersion;
+ QString code;
+ QLspSpecification::CompletionParams completionParams;
+ QLspSpecification::LSPPartialResponse<
+ std::variant<QList<QLspSpecification::CompletionItem>,
+ QLspSpecification::CompletionList, std::nullptr_t>,
+ std::variant<QLspSpecification::CompletionList,
+ QList<QLspSpecification::CompletionItem>>>
+ response;
+ void sendCompletions(QmlLsp::OpenDocumentSnapshot &);
+ QString urlAndPos() const;
+ QList<QLspSpecification::CompletionItem> completions(QmlLsp::OpenDocumentSnapshot &doc) const;
+};
+
+class QmlCompletionSupport : public QLanguageServerModule
+{
+ Q_OBJECT
+public:
+ QmlCompletionSupport(QmlLsp::QQmlCodeModel *codeModel);
+ ~QmlCompletionSupport();
+ QString name() const override;
+ void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override;
+ void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo,
+ QLspSpecification::InitializeResult &) override;
+public slots:
+ void updatedSnapshot(const QByteArray &uri);
+
+private:
+ QmlLsp::QQmlCodeModel *m_codeModel;
+ QMutex m_mutex;
+ QMultiHash<QString, CompletionRequest *> m_completions;
+};
+
+#endif // QMLCOMPLETIONSUPPORT_H
diff --git a/tools/qmlls/qmllanguageservertool.cpp b/tools/qmlls/qmllanguageservertool.cpp
index e1945a6cda..6b59e693df 100644
--- a/tools/qmlls/qmllanguageservertool.cpp
+++ b/tools/qmlls/qmllanguageservertool.cpp
@@ -133,6 +133,7 @@ int main(int argv, char *argc[])
QQmlToolingSettings settings(QLatin1String("qmlls"));
parser.setApplicationDescription(QLatin1String(R"(QML languageserver)"));
+ parser.addHelpOption();
QCommandLineOption waitOption(QStringList() << "w"
<< "wait",
QLatin1String("Waits the given number of seconds before startup"),
diff --git a/tools/qmlls/qqmlcodemodel.cpp b/tools/qmlls/qqmlcodemodel.cpp
index 7798e174e8..9251082b55 100644
--- a/tools/qmlls/qqmlcodemodel.cpp
+++ b/tools/qmlls/qqmlcodemodel.cpp
@@ -206,6 +206,7 @@ void QQmlCodeModel::indexDirectory(const QString &path, int depthLeft)
QFileInfo fInfo(fPath);
QString cPath = fInfo.canonicalFilePath();
if (!cPath.isEmpty()) {
+ newCurrent.loadBuiltins();
newCurrent.loadFile(cPath, fPath, [](Path, DomItem &, DomItem &) {}, {});
newCurrent.loadPendingDependencies();
newCurrent.commitToBase(m_validEnv.ownerAs<DomEnvironment>());
diff --git a/tools/qmlls/qqmllanguageserver.cpp b/tools/qmlls/qqmllanguageserver.cpp
index 5777f8747d..b2947e8131 100644
--- a/tools/qmlls/qqmllanguageserver.cpp
+++ b/tools/qmlls/qqmllanguageserver.cpp
@@ -69,12 +69,14 @@ QQmlLanguageServer::QQmlLanguageServer(std::function<void(const QByteArray &)> s
m_server(sendData),
m_textSynchronization(&m_codeModel),
m_lint(&m_server, &m_codeModel),
- m_workspace(&m_codeModel)
+ m_workspace(&m_codeModel),
+ m_completionSupport(&m_codeModel)
{
m_server.addServerModule(this);
m_server.addServerModule(&m_textSynchronization);
m_server.addServerModule(&m_lint);
m_server.addServerModule(&m_workspace);
+ m_server.addServerModule(&m_completionSupport);
m_server.finishSetup();
qCWarning(lspServerLog) << "Did Setup";
}
diff --git a/tools/qmlls/qqmllanguageserver.h b/tools/qmlls/qqmllanguageserver.h
index d9d17cc0e6..43615718ce 100644
--- a/tools/qmlls/qqmllanguageserver.h
+++ b/tools/qmlls/qqmllanguageserver.h
@@ -33,6 +33,7 @@
#include "textsynchronization.h"
#include "qmllintsuggestions.h"
#include "workspace.h"
+#include "qmlcompletionsupport.h"
#include "../shared/qqmltoolingsettings.h"
QT_BEGIN_NAMESPACE
@@ -81,6 +82,7 @@ private:
TextSynchronization m_textSynchronization;
QmlLintSuggestions m_lint;
WorkspaceHandlers m_workspace;
+ QmlCompletionSupport m_completionSupport;
int m_returnValue = 1;
};