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>
This commit is contained in:
Fawzi Mohamed 2022-03-14 14:51:13 +01:00
parent 2717985373
commit 538600cf5a
19 changed files with 1113 additions and 26 deletions

View File

@ -295,6 +295,24 @@ Q_ENUM_NS(WriteOutCheck)
Q_DECLARE_FLAGS(WriteOutChecks, WriteOutCheck)
Q_DECLARE_OPERATORS_FOR_FLAGS(WriteOutChecks)
enum class LocalSymbolsType {
None = 0x0,
QmlTypes = 0x1,
Types = 0x3,
Signals = 0x4,
Methods = 0xC,
Attributes = 0x10,
Ids = 0x20,
Components = 0x40,
Namespaces = 0x80,
Globals = 0x100,
MethodParameters = 0x200,
All = 0x3FF
};
Q_ENUM_NS(LocalSymbolsType)
Q_DECLARE_FLAGS(LocalSymbolsTypes, LocalSymbolsType)
Q_DECLARE_OPERATORS_FOR_FLAGS(LocalSymbolsTypes)
} // end namespace Dom
} // end namespace QQmlJS

View File

@ -351,6 +351,8 @@ public:
m_pragmas.append(pragma);
return Path::Field(Fields::pragmas).index(idx);
}
ImportScope &importScope() { return m_importScope; }
const ImportScope &importScope() const { return m_importScope; }
private:
friend class QmlDomAstCreator;

View File

@ -1573,48 +1573,82 @@ bool DomItem::visitScopeChain(function_ref<bool(DomItem &)> visitor, LookupOptio
return true;
}
QSet<QString> DomItem::localSymbolNames()
QSet<QString> DomItem::localSymbolNames(LocalSymbolsTypes typeFilter)
{
QSet<QString> res;
if (typeFilter == LocalSymbolsType::None)
return res;
switch (internalKind()) {
case DomType::QmlObject:
res += propertyDefs().keys();
res += bindings().keys();
res += methods().keys();
if (typeFilter & LocalSymbolsType::Attributes) {
res += propertyDefs().keys();
res += bindings().keys();
}
if (typeFilter & LocalSymbolsType::Methods) {
if ((typeFilter & LocalSymbolsType::Methods) == LocalSymbolsType::Methods) {
res += methods().keys();
} else {
bool shouldAddSignals = bool(typeFilter & LocalSymbolsType::Signals);
if (const QmlObject *objPtr = as<QmlObject>()) {
auto methods = objPtr->methods();
for (auto it = methods.cbegin(); it != methods.cend(); ++it) {
if (bool(it.value().methodType == MethodInfo::MethodType::Signal)
== shouldAddSignals)
res += it.key();
}
}
}
}
break;
case DomType::ScriptExpression:
// to do
break;
case DomType::QmlComponent:
res += ids().keys();
Q_FALLTHROUGH();
if (typeFilter & LocalSymbolsType::Ids)
res += ids().keys();
break;
case DomType::QmlFile: // subComponents, imported types
{
DomItem comps = field(Fields::components);
for (auto k : comps.keys())
if (!k.isEmpty())
if (typeFilter & LocalSymbolsType::Components) {
DomItem comps = field(Fields::components);
for (auto k : comps.keys())
if (!k.isEmpty())
res.insert(k);
}
break;
case DomType::ImportScope: {
const ImportScope *currentPtr = as<ImportScope>();
if (typeFilter & LocalSymbolsType::Types) {
if ((typeFilter & LocalSymbolsType::Types) == LocalSymbolsType::Types) {
res += currentPtr->importedNames(*this);
} else {
bool qmlTypes = bool(typeFilter & LocalSymbolsType::QmlTypes);
for (const QString &typeName : currentPtr->importedNames(*this)) {
if ((!typeName.isEmpty() && typeName.at(0).isUpper()) == qmlTypes)
res += typeName;
}
}
}
if (typeFilter & LocalSymbolsType::Namespaces) {
for (const auto &k : currentPtr->subImports().keys())
res.insert(k);
}
break;
}
case DomType::QmltypesComponent:
case DomType::JsResource:
case DomType::GlobalComponent:
res += enumerations().keys();
if (typeFilter & LocalSymbolsType::Globals)
res += enumerations().keys();
break;
case DomType::MethodInfo: {
DomItem params = field(Fields::parameters);
params.visitIndexes([&res](DomItem &p) {
const MethodParameter *pPtr = p.as<MethodParameter>();
res.insert(pPtr->name);
return true;
});
break;
}
case DomType::ImportScope: {
const ImportScope *currentPtr = as<ImportScope>();
res += currentPtr->importedNames(*this);
for (auto k : currentPtr->subImports().keys())
res.insert(k);
if (typeFilter & LocalSymbolsType::MethodParameters) {
DomItem params = field(Fields::parameters);
params.visitIndexes([&res](DomItem &p) {
const MethodParameter *pPtr = p.as<MethodParameter>();
res.insert(pPtr->name);
return true;
});
}
break;
}
default:
@ -1627,6 +1661,21 @@ bool DomItem::visitLookup1(QString symbolName, function_ref<bool(DomItem &)> vis
LookupOptions opts, ErrorHandler h, QSet<quintptr> *visited,
QList<Path> *visitedRefs)
{
bool typeLookupInQmlFile = symbolName.length() > 1 && symbolName.at(0).isUpper()
&& fileObject().internalKind() == DomType::QmlFile;
if (typeLookupInQmlFile) {
// shortcut to lookup types (scope chain would find them too, but after looking
// the prototype chain)
DomItem importScope = fileObject().field(Fields::importScope);
if (const ImportScope *importScopePtr = importScope.as<ImportScope>()) {
QList<DomItem> types = importScopePtr->importedItemsWithName(importScope, symbolName);
for (DomItem &t : types) {
if (!visitor(t))
return false;
}
}
return true;
}
return visitScopeChain(
[symbolName, visitor](DomItem &obj) {
return obj.visitLocalSymbolsNamed(symbolName,

View File

@ -879,7 +879,7 @@ public:
LookupOptions = LookupOption::Normal, ErrorHandler h = nullptr,
QSet<quintptr> *visited = nullptr, QList<Path> *visitedRefs = nullptr);
bool visitLocalSymbolsNamed(QString name, function_ref<bool(DomItem &)> visitor);
QSet<QString> localSymbolNames();
QSet<QString> localSymbolNames(LocalSymbolsTypes lTypes = LocalSymbolsType::All);
bool visitLookup1(QString symbolName, function_ref<bool(DomItem &)> visitor,
LookupOptions = LookupOption::Normal, ErrorHandler h = nullptr,
QSet<quintptr> *visited = nullptr, QList<Path> *visitedRefs = nullptr);

View File

@ -1,4 +1,5 @@
if (TARGET Qt::LanguageServerPrivate)
add_subdirectory(qmlls)
add_subdirectory(lifecycle)
add_subdirectory(completions)
endif()

View File

@ -0,0 +1,24 @@
file(GLOB_RECURSE test_data_glob
RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
data)
list(APPEND test_data ${test_data_glob})
qt_internal_add_test(tst_qmllscompletions
SOURCES
tst_qmllscompletions.cpp
DEFINES
QT_DEPRECATED_WARNINGS
QT_QMLTEST_DATADIR=\\\"${CMAKE_CURRENT_SOURCE_DIR}/data\\\"
PUBLIC_LIBRARIES
Qt::Core
Qt::QmlDomPrivate
Qt::LanguageServerPrivate
Qt::Test
Qt::QuickTestUtilsPrivate
TESTDATA ${test_data}
)
qt_internal_extend_target(tst_qmllscompletions CONDITION ANDROID OR IOS
DEFINES
QT_QMLTEST_DATADIR=\\\":/domdata\\\"
)

View File

@ -0,0 +1,7 @@
import QtQuick
Rectangle {
color: "red"
function blabla() {}
}

View File

@ -0,0 +1,2 @@
module BuildDir
BuildDirType 1.0 BuildDirType.qml

View File

@ -0,0 +1,12 @@
import QtQuick 2.0
Zzz {
width: height
Rectangle {
color: "green"
anchors.fill: parent
}
function lala() {}
}

View File

@ -0,0 +1,10 @@
import QtQuick 2.11
Item {
id: zzz
height: 333
Rectangle {
width: zzz.height
}
}

View File

@ -0,0 +1,9 @@
import BuildDir
BuildDirType {
Rectangle {
color: "green"
width: 250
height: 10
}
}

View File

@ -0,0 +1,219 @@
/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite 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 <QtJsonRpc/private/qjsonrpcprotocol_p.h>
#include <QtLanguageServer/private/qlanguageserverprotocol_p.h>
#include <QtQuickTestUtils/private/qmlutils_p.h>
#include <QtCore/qobject.h>
#include <QtCore/qprocess.h>
#include <QtCore/qlibraryinfo.h>
#include <QtCore/qstringlist.h>
#include <QtTest/qtest.h>
#include <iostream>
#include <variant>
QT_USE_NAMESPACE
using namespace Qt::StringLiterals;
using namespace QLspSpecification;
class tst_QmllsCompletions : public QQmlDataTest
{
Q_OBJECT
public:
tst_QmllsCompletions();
private slots:
void initTestCase() final;
void completions_data();
void completions();
void cleanupTestCase();
private:
QProcess m_server;
QLanguageServerProtocol m_protocol;
QString m_qmllsPath;
QList<QByteArray> m_uriToClose;
};
tst_QmllsCompletions::tst_QmllsCompletions()
: QQmlDataTest(QT_QMLTEST_DATADIR),
m_protocol([this](const QByteArray &data) { m_server.write(data); })
{
connect(&m_server, &QProcess::readyReadStandardOutput, this, [this]() {
QByteArray data = m_server.readAllStandardOutput();
m_protocol.receiveData(data);
});
connect(&m_server, &QProcess::readyReadStandardError, this,
[this]() { qWarning() << "LSPerr" << m_server.readAllStandardError(); });
m_qmllsPath = QLibraryInfo::path(QLibraryInfo::BinariesPath) + QLatin1String("/qmlls");
#ifdef Q_OS_WIN
m_qmllsPath += QLatin1String(".exe");
#endif
// allow overriding of the executable, to be able to use a qmlEcho script (as described in
// qmllanguageservertool.cpp)
m_qmllsPath = qEnvironmentVariable("QMLLS", m_qmllsPath);
m_server.setProgram(m_qmllsPath);
m_protocol.registerPublishDiagnosticsNotificationHandler([](const QByteArray &, auto) {
// ignoring qmlint notifications
});
}
void tst_QmllsCompletions::initTestCase()
{
QQmlDataTest::initTestCase();
if (!QFileInfo::exists(m_qmllsPath)) {
QString message =
QStringLiteral("qmlls executable not found (looked for %0)").arg(m_qmllsPath);
QSKIP(qPrintable(message)); // until we add a feature for this we avoid failing here
}
m_server.start();
InitializeParams clientInfo;
clientInfo.rootUri = QUrl::fromLocalFile(dataDirectory() + "/default").toString().toUtf8();
TextDocumentClientCapabilities tDoc;
PublishDiagnosticsClientCapabilities pDiag;
tDoc.publishDiagnostics = pDiag;
pDiag.versionSupport = true;
clientInfo.capabilities.textDocument = tDoc;
bool didInit = false;
m_protocol.requestInitialize(clientInfo, [this, &didInit](const InitializeResult &serverInfo) {
Q_UNUSED(serverInfo);
m_protocol.notifyInitialized(InitializedParams());
didInit = true;
});
QTRY_COMPARE_WITH_TIMEOUT(didInit, true, 10000);
QFile file(testFile("completions/Yyy.qml"));
QVERIFY(file.open(QIODevice::ReadOnly));
DidOpenTextDocumentParams oParams;
TextDocumentItem textDocument;
QByteArray uri = testFileUrl("completions/Yyy.qml").toString().toUtf8();
textDocument.uri = uri;
textDocument.text = file.readAll();
oParams.textDocument = textDocument;
m_protocol.notifyDidOpenTextDocument(oParams);
m_uriToClose.append(uri);
}
void tst_QmllsCompletions::completions_data()
{
QTest::addColumn<QByteArray>("uri");
QTest::addColumn<int>("lineNr");
QTest::addColumn<int>("character");
QTest::addColumn<QStringList>("expected");
QTest::addColumn<QStringList>("notExpected");
QByteArray uri = testFileUrl("completions/Yyy.qml").toString().toUtf8();
QTest::newRow("objEmptyLine") << uri << 7 << 0
<< QStringList({ u"Rectangle"_s, u"property"_s, u"width"_s,
u"function"_s })
<< QStringList({ u"QtQuick"_s, u"vector4d"_s });
QTest::newRow("inBindingLabel")
<< uri << 3 << 7 << QStringList({ u"Rectangle"_s, u"property"_s, u"width"_s })
<< QStringList({ u"QtQuick"_s, u"vector4d"_s });
QTest::newRow("fileStart") << uri << 0 << 0 << QStringList({ u"Rectangle"_s, u"import"_s })
<< QStringList({ u"QtQuick"_s, u"vector4d"_s, u"width"_s });
QTest::newRow("importImport") << uri << 0 << 3 << QStringList({ u"Rectangle"_s, u"import"_s })
<< QStringList({ u"QtQuick"_s, u"vector4d"_s, u"width"_s });
QTest::newRow("importModuleStart")
<< uri << 0 << 7 << QStringList({ u"QtQuick"_s })
<< QStringList({ u"vector4d"_s, u"width"_s, u"Rectangle"_s, u"import"_s });
QTest::newRow("importVersionStart")
<< uri << 0 << 15 << QStringList({ u"2"_s, u"as"_s })
<< QStringList({ u"Rectangle"_s, u"import"_s, u"vector4d"_s, u"width"_s });
// QTest::newRow("importVersionMinor") << uri << 0 << 17 << QStringList({u"15"_s})
// << QStringList({u"as"_s, u"Rectangle"_s, u"import"_s,
// u"vector4d"_s, u"width"_s});
QTest::newRow("inScript") << uri << 5 << 14
<< QStringList(
{ u"Rectangle"_s, u"vector4d"_s, u"lala()"_s, u"width"_s })
<< QStringList({ u"import"_s });
}
void tst_QmllsCompletions::completions()
{
QFETCH(QByteArray, uri);
QFETCH(int, lineNr);
QFETCH(int, character);
QFETCH(QStringList, expected);
QFETCH(QStringList, notExpected);
CompletionParams cParams;
cParams.position.line = lineNr;
cParams.position.character = character;
cParams.textDocument.uri = uri;
std::shared_ptr<bool> didFinish = std::make_shared<bool>(false);
auto clean = [didFinish]() { *didFinish = true; };
m_protocol.requestCompletion(
cParams,
[clean, uri, expected, notExpected](auto res) {
QSet<QString> labels;
if (const QList<CompletionItem> *cItems =
std::get_if<QList<CompletionItem>>(&res)) {
for (const CompletionItem &c : *cItems)
labels << c.label;
}
for (const QString &exp : expected) {
QVERIFY2(labels.contains(exp),
u"no %1 in %2"_s
.arg(exp,
QStringList(labels.begin(), labels.end()).join(u", "_s))
.toUtf8());
}
for (const QString &nexp : notExpected) {
QVERIFY2(!labels.contains(nexp),
u"found unexpected completion %1"_s.arg(nexp).toUtf8());
}
clean();
},
[clean](const ResponseError &err) {
ProtocolBase::defaultResponseErrorHandler(err);
QVERIFY2(false, "error computing the completion");
clean();
});
QTRY_VERIFY_WITH_TIMEOUT(*didFinish, 30000);
}
void tst_QmllsCompletions::cleanupTestCase()
{
for (const QByteArray &uri : m_uriToClose) {
DidCloseTextDocumentParams closeP;
closeP.textDocument.uri = uri;
m_protocol.notifyDidCloseTextDocument(closeP);
}
m_server.closeWriteChannel();
QTRY_COMPARE(m_server.state(), QProcess::NotRunning);
QCOMPARE(m_server.exitStatus(), QProcess::NormalExit);
}
QTEST_MAIN(tst_QmllsCompletions)
#include <tst_qmllscompletions.moc>

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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"),

View File

@ -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>());

View File

@ -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";
}

View File

@ -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;
};