aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSami Shalayel <[email protected]>2024-02-12 15:49:18 +0100
committerSami Shalayel <[email protected]>2024-02-29 12:08:07 +0100
commit6cdf8b829051b548463b682e7ba7c2256758ea81 (patch)
treeeff689facbe57857e235982a6da3e5b9cd2d6e2e
parent6680c94906f8cceaecf38d0b211c7185ef4d8102 (diff)
Introduce plugin support for qmlls and qmlls quick plugin
Introduce the plugin support for qmlls, and add an (empty) snippet qmlls plugin that will provide quick snippets in the future. Add the QQmlLSPlugin plugin interface, and a QmlLSQuickPlugin that implements it. Use QFactoryLoader to load the plugins. Move the quick snippet code from qqmllscompletion to the newly introduced quick qmlls plugin for snippet completion. Add a test in tst_qmlls_modules to make sure that the plugin is loaded correctly in qmlls. Fixes: QTBUG-119969 Change-Id: If9cfedfbf166aff0eca8fe7008b0564035cd0153 Reviewed-by: Fabian Kosmale <[email protected]> Reviewed-by: Qt CI Bot <[email protected]>
-rw-r--r--src/plugins/CMakeLists.txt6
-rw-r--r--src/plugins/qmlls/CMakeLists.txt6
-rw-r--r--src/plugins/qmlls/quick/CMakeLists.txt14
-rw-r--r--src/plugins/qmlls/quick/plugin.json8
-rw-r--r--src/plugins/qmlls/quick/qqmllsquickplugin.cpp187
-rw-r--r--src/plugins/qmlls/quick/qqmllsquickplugin_p.h47
-rw-r--r--src/qmlls/CMakeLists.txt3
-rw-r--r--src/qmlls/qqmlcodemodel.cpp4
-rw-r--r--src/qmlls/qqmlcodemodel_p.h3
-rw-r--r--src/qmlls/qqmlcompletionsupport.cpp5
-rw-r--r--src/qmlls/qqmlcompletionsupport_p.h2
-rw-r--r--src/qmlls/qqmllscompletion.cpp192
-rw-r--r--src/qmlls/qqmllscompletion_p.h25
-rw-r--r--src/qmlls/qqmllscompletionplugin.cpp4
-rw-r--r--src/qmlls/qqmllscompletionplugin_p.h42
-rw-r--r--src/qmlls/qqmllsplugin_p.h42
-rw-r--r--tests/auto/qmlls/modules/tst_qmlls_modules.cpp16
-rw-r--r--tests/auto/qmlls/modules/tst_qmlls_modules.h1
-rw-r--r--tests/auto/qmlls/utils/tst_qmlls_utils.cpp2
-rw-r--r--tests/auto/qmlls/utils/tst_qmlls_utils.h8
-rw-r--r--tools/qmlls/CMakeLists.txt5
21 files changed, 471 insertions, 151 deletions
diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt
index 548ef486f0..42c7c2332b 100644
--- a/src/plugins/CMakeLists.txt
+++ b/src/plugins/CMakeLists.txt
@@ -1,8 +1,6 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
-# Generated from plugins.pro.
-
if(QT_FEATURE_qml_debug AND QT_FEATURE_thread)
add_subdirectory(qmltooling)
endif()
@@ -11,3 +9,7 @@ if(TARGET Qt::Quick)
endif()
add_subdirectory(qmllint)
+
+if(TARGET Qt::QmlLSPrivate)
+ add_subdirectory(qmlls)
+endif()
diff --git a/src/plugins/qmlls/CMakeLists.txt b/src/plugins/qmlls/CMakeLists.txt
new file mode 100644
index 0000000000..32e53a51c4
--- /dev/null
+++ b/src/plugins/qmlls/CMakeLists.txt
@@ -0,0 +1,6 @@
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+if (TARGET Qt::Quick)
+ add_subdirectory(quick)
+endif()
diff --git a/src/plugins/qmlls/quick/CMakeLists.txt b/src/plugins/qmlls/quick/CMakeLists.txt
new file mode 100644
index 0000000000..d306956027
--- /dev/null
+++ b/src/plugins/qmlls/quick/CMakeLists.txt
@@ -0,0 +1,14 @@
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_plugin(QmlLSQuickPlugin
+ CLASS_NAME QQmlLSQuickPlugin
+ OUTPUT_NAME qmllsquickplugin
+ PLUGIN_TYPE qmlls
+ SOURCES
+ qqmllsquickplugin_p.h
+ qqmllsquickplugin.cpp
+ LIBRARIES
+ Qt::QmlCompilerPrivate
+ Qt::QmlLSPrivate
+)
diff --git a/src/plugins/qmlls/quick/plugin.json b/src/plugins/qmlls/quick/plugin.json
new file mode 100644
index 0000000000..42978a9968
--- /dev/null
+++ b/src/plugins/qmlls/quick/plugin.json
@@ -0,0 +1,8 @@
+{
+ "name": "QmlLSQuick",
+ "author": "Qt",
+ "description": "Provide QmlLS utilities for QtQuick",
+ "version": "1.0",
+ "isInternal": true,
+ "Keys" : [ "quick" ]
+}
diff --git a/src/plugins/qmlls/quick/qqmllsquickplugin.cpp b/src/plugins/qmlls/quick/qqmllsquickplugin.cpp
new file mode 100644
index 0000000000..67b95b074a
--- /dev/null
+++ b/src/plugins/qmlls/quick/qqmllsquickplugin.cpp
@@ -0,0 +1,187 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "qqmllsquickplugin_p.h"
+#include <QtQmlLS/private/qqmllsutils_p.h>
+#include <QtQmlLS/private/qqmllscompletion_p.h>
+
+using namespace QLspSpecification;
+using namespace QQmlJS::Dom;
+
+QT_BEGIN_NAMESPACE
+
+std::unique_ptr<QQmlLSCompletionPlugin> QQmlLSQuickPlugin::createCompletionPlugin() const
+{
+ return std::make_unique<QQmlLSQuickCompletionPlugin>();
+}
+
+void QQmlLSQuickCompletionPlugin::suggestSnippetsForLeftHandSideOfBinding(
+ const DomItem &itemAtPosition, BackInsertIterator result) const
+{
+ auto file = itemAtPosition.containingFile().as<QmlFile>();
+ if (!file)
+ return;
+
+ // check if QtQuick has been imported
+ const auto &imports = file->imports();
+ auto it = std::find_if(imports.constBegin(), imports.constEnd(), [](const Import &import) {
+ return import.uri.moduleUri() == u"QtQuick";
+ });
+ if (it == imports.constEnd()) {
+ return;
+ }
+
+ // for default bindings:
+ suggestSnippetsForRightHandSideOfBinding(itemAtPosition, result);
+
+ // check if the user already typed some qualifier, remove its dot and compare it to QtQuick's
+ // qualified name
+ const QString userTypedQualifier = QQmlLSUtils::qualifiersFrom(itemAtPosition);
+ if (!userTypedQualifier.isEmpty()
+ && !it->importId.startsWith(QStringView(userTypedQualifier).chopped(1))) {
+ return;
+ }
+
+ const QByteArray prefixForSnippet =
+ userTypedQualifier.isEmpty() ? it->importId.toUtf8() : QByteArray();
+ const QByteArray prefixWithDotForSnippet =
+ prefixForSnippet.isEmpty() ? QByteArray() : QByteArray(prefixForSnippet).append(u'.');
+
+ auto resolver = file->typeResolver();
+ if (!resolver)
+ return;
+ const auto qquickItemScope = resolver->typeForName(prefixWithDotForSnippet + u"Item"_s);
+ const QQmlJSScope::ConstPtr ownerScope = itemAtPosition.qmlObject().semanticScope();
+ if (!ownerScope || !qquickItemScope)
+ return;
+
+ if (ownerScope->inherits(qquickItemScope)) {
+ result = QQmlLSCompletion::makeSnippet(
+ "states binding with PropertyChanges in State",
+ "states: [\n"
+ "\t"_ba.append(prefixWithDotForSnippet)
+ .append("State {\n"
+ "\t\tname: \"${1:name}\"\n"
+ "\t\t"_ba.append(prefixWithDotForSnippet)
+ .append("PropertyChanges {\n"
+ "\t\t\ttarget: ${2:object}\n"
+ "\t\t}\n"
+ "\t}\n"
+ "]")));
+ result = QQmlLSCompletion::makeSnippet("transitions binding with Transition",
+ "transitions: [\n"
+ "\t"_ba.append(prefixWithDotForSnippet)
+ .append("Transition {\n"
+ "\t\tfrom: \"${1:fromState}\"\n"
+ "\t\tto: \"${2:fromState}\"\n"
+ "\t}\n"
+ "]"));
+ }
+}
+
+void QQmlLSQuickCompletionPlugin::suggestSnippetsForRightHandSideOfBinding(
+ const DomItem &itemAtPosition, BackInsertIterator result) const
+{
+ auto file = itemAtPosition.containingFile().as<QmlFile>();
+ if (!file)
+ return;
+
+ // check if QtQuick has been imported
+ const auto &imports = file->imports();
+ auto it = std::find_if(imports.constBegin(), imports.constEnd(), [](const Import &import) {
+ return import.uri.moduleUri() == u"QtQuick";
+ });
+ if (it == imports.constEnd()) {
+ return;
+ }
+
+ // check if the user already typed some qualifier, remove its dot and compare it to QtQuick's
+ // qualified name
+ const QString userTypedQualifier = QQmlLSUtils::qualifiersFrom(itemAtPosition);
+ if (!userTypedQualifier.isEmpty()
+ && !it->importId.startsWith(QStringView(userTypedQualifier).chopped(1))) {
+ return;
+ }
+
+ const QByteArray prefixForSnippet =
+ userTypedQualifier.isEmpty() ? it->importId.toUtf8() : QByteArray();
+ const QByteArray prefixWithDotForSnippet =
+ prefixForSnippet.isEmpty() ? QByteArray() : QByteArray(prefixForSnippet).append(u'.');
+
+ // Quick completions from Qt Creator's code model
+ result = QQmlLSCompletion::makeSnippet(prefixForSnippet, "BorderImage snippet",
+ "BorderImage {\n"
+ "\tid: ${1:name}\n"
+ "\tsource: \"${2:file}\"\n"
+ "\twidth: ${3:100}; height: ${4:100}\n"
+ "\tborder.left: ${5: 5}; border.top: ${5}\n"
+ "\tborder.right: ${5}; border.bottom: ${5}\n"
+ "}");
+ result = QQmlLSCompletion::makeSnippet(prefixForSnippet, "ColorAnimation snippet",
+ "ColorAnimation {\n"
+ "\tfrom: \"${1:white}\"\n"
+ "\tto: \"${2:black}\"\n"
+ "\tduration: ${3:200}\n"
+ "}");
+ result = QQmlLSCompletion::makeSnippet(prefixForSnippet, "Image snippet",
+ "Image {\n"
+ "\tid: ${1:name}\n"
+ "\tsource: \"${2:file}\"\n"
+ "}");
+ result = QQmlLSCompletion::makeSnippet(prefixForSnippet, "Item snippet",
+ "Item {\n"
+ "\tid: ${1:name}\n"
+ "}");
+ result = QQmlLSCompletion::makeSnippet(prefixForSnippet, "NumberAnimation snippet",
+ "NumberAnimation {\n"
+ "\ttarget: ${1:object}\n"
+ "\tproperty: \"${2:name}\"\n"
+ "\tduration: ${3:200}\n"
+ "\teasing.type: "_ba.append(prefixWithDotForSnippet)
+ .append("Easing.${4:InOutQuad}\n"
+ "}"));
+ result = QQmlLSCompletion::makeSnippet(prefixForSnippet, "NumberAnimation with targets snippet",
+ "NumberAnimation {\n"
+ "\ttargets: [${1:object}]\n"
+ "\tproperties: \"${2:name}\"\n"
+ "\tduration: ${3:200}\n"
+ "}");
+ result = QQmlLSCompletion::makeSnippet(prefixForSnippet, "PauseAnimation snippet",
+ "PauseAnimation {\n"
+ "\tduration: ${1:200}\n"
+ "}");
+ result = QQmlLSCompletion::makeSnippet(prefixForSnippet, "PropertyAction snippet",
+ "PropertyAction {\n"
+ "\ttarget: ${1:object}\n"
+ "\tproperty: \"${2:name}\"\n"
+ "}");
+ result = QQmlLSCompletion::makeSnippet(prefixForSnippet, "PropertyAction with targets snippet",
+ "PropertyAction {\n"
+ "\ttargets: [${1:object}]\n"
+ "\tproperties: \"${2:name}\"\n"
+ "}");
+ result = QQmlLSCompletion::makeSnippet(prefixForSnippet, "PropertyChanges snippet",
+ "PropertyChanges {\n"
+ "\ttarget: ${1:object}\n"
+ "}");
+ result = QQmlLSCompletion::makeSnippet(prefixForSnippet, "State snippet",
+ "State {\n"
+ "\tname: ${1:name}\n"
+ "\t"_ba.append(prefixWithDotForSnippet)
+ .append("PropertyChanges {\n"
+ "\t\ttarget: ${2:object}\n"
+ "\t}\n"
+ "}"));
+ result = QQmlLSCompletion::makeSnippet(prefixForSnippet, "Text snippet",
+ "Text {\n"
+ "\tid: ${1:name}\n"
+ "\ttext: qsTr(\"${2:text}\")\n"
+ "}");
+ result = QQmlLSCompletion::makeSnippet(prefixForSnippet, "Transition snippet",
+ "Transition {\n"
+ "\tfrom: \"${1:fromState}\"\n"
+ "\tto: \"${2:toState}\"\n"
+ "}");
+}
+
+QT_END_NAMESPACE
diff --git a/src/plugins/qmlls/quick/qqmllsquickplugin_p.h b/src/plugins/qmlls/quick/qqmllsquickplugin_p.h
new file mode 100644
index 0000000000..da6bc6f827
--- /dev/null
+++ b/src/plugins/qmlls/quick/qqmllsquickplugin_p.h
@@ -0,0 +1,47 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#ifndef QMLLSQUICKPLUGIN_H
+#define QMLLSQUICKPLUGIN_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtCore/qplugin.h>
+
+#include <QtQmlLS/private/qqmllsplugin_p.h>
+#include <QtQmlDom/private/qqmldomitem_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQmlLSQuickCompletionPlugin : public QQmlLSCompletionPlugin
+{
+public:
+ void suggestSnippetsForLeftHandSideOfBinding(const QQmlJS::Dom::DomItem &items,
+ BackInsertIterator result) const override;
+
+ void suggestSnippetsForRightHandSideOfBinding(const QQmlJS::Dom::DomItem &items,
+ BackInsertIterator result) const override;
+};
+
+
+class QQmlLSQuickPlugin : public QObject, QQmlLSPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID QmlLSPluginInterface_iid FILE "plugin.json")
+ Q_INTERFACES(QQmlLSPlugin)
+public:
+ std::unique_ptr<QQmlLSCompletionPlugin> createCompletionPlugin() const override;
+};
+
+QT_END_NAMESPACE
+
+#endif // QMLLSQUICKPLUGIN_H
diff --git a/src/qmlls/CMakeLists.txt b/src/qmlls/CMakeLists.txt
index 1e0f0bc1b0..1ef07c18f4 100644
--- a/src/qmlls/CMakeLists.txt
+++ b/src/qmlls/CMakeLists.txt
@@ -4,6 +4,7 @@
qt_internal_add_module(QmlLSPrivate
STATIC
INTERNAL_MODULE
+ PLUGIN_TYPES qmlls
SOURCES
qlspcustomtypes_p.h
qlanguageserver_p.h qlanguageserver_p.h qlanguageserver.cpp
@@ -26,7 +27,9 @@ qt_internal_add_module(QmlLSPrivate
qqmlrenamesymbolsupport_p.h qqmlrenamesymbolsupport.cpp
qqmlcompletioncontextstrings_p.h qqmlcompletioncontextstrings.cpp
qqmlhover_p.h qqmlhover.cpp
+ qqmllsplugin_p.h
qqmllscompletion.cpp qqmllscompletion_p.h
+ qqmllscompletionplugin.cpp qqmllscompletionplugin_p.h
PUBLIC_LIBRARIES
Qt::LanguageServerPrivate
diff --git a/src/qmlls/qqmlcodemodel.cpp b/src/qmlls/qqmlcodemodel.cpp
index d34ec8ef4e..252a93dd8c 100644
--- a/src/qmlls/qqmlcodemodel.cpp
+++ b/src/qmlls/qqmlcodemodel.cpp
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "qqmlcodemodel_p.h"
+#include "qqmllsplugin_p.h"
#include "qtextdocument_p.h"
#include "qqmllsutils_p.h"
@@ -88,7 +89,8 @@ QQmlCodeModel::QQmlCodeModel(QObject *parent, QQmlToolingSettings *settings)
m_validEnv(std::make_shared<DomEnvironment>(
QStringList(QLibraryInfo::path(QLibraryInfo::QmlImportsPath)),
DomEnvironment::Option::SingleThreaded)),
- m_settings(settings)
+ m_settings(settings),
+ m_pluginLoader(QmlLSPluginInterface_iid, u"/qmlls"_s)
{
}
diff --git a/src/qmlls/qqmlcodemodel_p.h b/src/qmlls/qqmlcodemodel_p.h
index a31cb16ae7..1be4ca13d2 100644
--- a/src/qmlls/qqmlcodemodel_p.h
+++ b/src/qmlls/qqmlcodemodel_p.h
@@ -21,6 +21,7 @@
#include <QObject>
#include <QHash>
#include <QtCore/qfilesystemwatcher.h>
+#include <QtCore/private/qfactoryloader_p.h>
#include <QtQmlDom/private/qqmldomitem_p.h>
#include <QtQmlCompiler/private/qqmljsscope_p.h>
#include <QtQmlToolingSettings/private/qqmltoolingsettings_p.h>
@@ -105,6 +106,7 @@ public:
QStringList findFilePathsFromFileNames(const QStringList &fileNames) const;
static QStringList fileNamesToWatch(const QQmlJS::Dom::DomItem &qmlFile);
void disableCMakeCalls();
+ const QFactoryLoader &pluginLoader() const { return m_pluginLoader; }
Q_SIGNALS:
void updatedSnapshot(const QByteArray &url);
private:
@@ -145,6 +147,7 @@ private:
QHash<QByteArray, OpenDocument> m_openDocuments;
QQmlToolingSettings *m_settings;
QFileSystemWatcher m_cppFileWatcher;
+ QFactoryLoader m_pluginLoader;
bool m_rebuildRequired = true; // always trigger a rebuild on start
CMakeStatus m_cmakeStatus = RequiresInitialization;
private slots:
diff --git a/src/qmlls/qqmlcompletionsupport.cpp b/src/qmlls/qqmlcompletionsupport.cpp
index 0919a21687..b6938681cf 100644
--- a/src/qmlls/qqmlcompletionsupport.cpp
+++ b/src/qmlls/qqmlcompletionsupport.cpp
@@ -39,6 +39,11 @@ bool CompletionRequest::fillFrom(QmlLsp::OpenDocument doc, const Parameters &par
return true;
}
+QmlCompletionSupport::QmlCompletionSupport(QmlLsp::QQmlCodeModel *codeModel)
+ : BaseT(codeModel), m_completionEngine(codeModel->pluginLoader())
+{
+}
+
void QmlCompletionSupport::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol)
{
protocol->registerCompletionRequestHandler(getRequestHandler());
diff --git a/src/qmlls/qqmlcompletionsupport_p.h b/src/qmlls/qqmlcompletionsupport_p.h
index 7add1615b7..fa0c1ca51a 100644
--- a/src/qmlls/qqmlcompletionsupport_p.h
+++ b/src/qmlls/qqmlcompletionsupport_p.h
@@ -46,7 +46,7 @@ class QmlCompletionSupport : public QQmlBaseModule<CompletionRequest>
{
Q_OBJECT
public:
- using BaseT::BaseT;
+ QmlCompletionSupport(QmlLsp::QQmlCodeModel *codeModel);
QString name() const override;
void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override;
void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo,
diff --git a/src/qmlls/qqmllscompletion.cpp b/src/qmlls/qqmllscompletion.cpp
index 658034d935..c9c96ead02 100644
--- a/src/qmlls/qqmllscompletion.cpp
+++ b/src/qmlls/qqmllscompletion.cpp
@@ -670,7 +670,7 @@ void QQmlLSCompletion::insideQmlObjectCompletion(const DomItem &parentForContext
options.setFlag(LocalSymbolsType::ObjectType);
suggestReachableTypes(positionInfo.itemAtPosition, options, CompletionItemKind::Constructor,
result);
- suggestQuickSnippetsCompletion(positionInfo.itemAtPosition, result);
+ suggestSnippetsForLeftHandSideOfBinding(positionInfo.itemAtPosition, result);
if (QQmlLSUtils::isFieldMemberExpression(positionInfo.itemAtPosition)) {
/*!
@@ -744,7 +744,7 @@ void QQmlLSCompletion::insideQmlObjectCompletion(const DomItem &parentForContext
const DomItem containingFile = parentForContext.containingFile();
suggestReachableTypes(containingFile, LocalSymbolsType::ObjectType,
CompletionItemKind::Constructor, result);
- suggestQuickSnippetsCompletion(positionInfo.itemAtPosition, result);
+ suggestSnippetsForLeftHandSideOfBinding(positionInfo.itemAtPosition, result);
return;
}
}
@@ -832,7 +832,7 @@ void QQmlLSCompletion::insideBindingCompletion(const DomItem &currentItem,
options.setFlag(LocalSymbolsType::ObjectType);
suggestReachableTypes(positionInfo.itemAtPosition, options,
CompletionItemKind::Constructor, result);
- suggestQuickSnippetsCompletion(positionInfo.itemAtPosition, result);
+ suggestSnippetsForRightHandSideOfBinding(positionInfo.itemAtPosition, result);
}
}
return;
@@ -851,7 +851,7 @@ void QQmlLSCompletion::insideBindingCompletion(const DomItem &currentItem,
// add Qml Types for default binding
suggestReachableTypes(positionInfo.itemAtPosition, LocalSymbolsType::ObjectType,
CompletionItemKind::Constructor, result);
- suggestQuickSnippetsCompletion(positionInfo.itemAtPosition, result);
+ suggestSnippetsForLeftHandSideOfBinding(positionInfo.itemAtPosition, result);
}
void QQmlLSCompletion::insideImportCompletion(const DomItem &currentItem,
@@ -1631,144 +1631,6 @@ void QQmlLSCompletion::signalHandlerCompletion(const QQmlJSScope::ConstPtr &scop
}
}
-void QQmlLSCompletion::suggestQuickSnippetsCompletion(const DomItem &itemAtPosition,
- BackInsertIterator result) const
-{
- auto file = itemAtPosition.containingFile().as<QmlFile>();
- if (!file)
- return;
-
- // check if QtQuick has been imported
- const auto &imports = file->imports();
- auto it = std::find_if(imports.constBegin(), imports.constEnd(), [](const Import &import) {
- return import.uri.moduleUri() == u"QtQuick";
- });
- if (it == imports.constEnd()) {
- return;
- }
-
- // check if the user already typed some qualifier, remove its dot and compare it to QtQuick's
- // qualified name
- const QString userTypedQualifier = QQmlLSUtils::qualifiersFrom(itemAtPosition);
- if (!userTypedQualifier.isEmpty()
- && !it->importId.startsWith(QStringView(userTypedQualifier).chopped(1))) {
- return;
- }
-
- const QByteArray prefixForSnippet =
- userTypedQualifier.isEmpty() ? it->importId.toUtf8() : QByteArray();
- const QByteArray prefixWithDotForSnippet =
- prefixForSnippet.isEmpty() ? QByteArray() : QByteArray(prefixForSnippet).append(u'.');
-
- // Quick completions from Qt Creator's code model
- result = makeSnippet(prefixForSnippet, "BorderImage snippet",
- "BorderImage {\n"
- "\tid: ${1:name}\n"
- "\tsource: \"${2:file}\"\n"
- "\twidth: ${3:100}; height: ${4:100}\n"
- "\tborder.left: ${5: 5}; border.top: ${5}\n"
- "\tborder.right: ${5}; border.bottom: ${5}\n"
- "}");
- result = makeSnippet(prefixForSnippet, "ColorAnimation snippet",
- "ColorAnimation {\n"
- "\tfrom: \"${1:white}\"\n"
- "\tto: \"${2:black}\"\n"
- "\tduration: ${3:200}\n"
- "}");
- result = makeSnippet(prefixForSnippet, "Image snippet",
- "Image {\n"
- "\tid: ${1:name}\n"
- "\tsource: \"${2:file}\"\n"
- "}");
- result = makeSnippet(prefixForSnippet, "Item snippet",
- "Item {\n"
- "\tid: ${1:name}\n"
- "}");
- result = makeSnippet(prefixForSnippet, "NumberAnimation snippet",
- "NumberAnimation {\n"
- "\ttarget: ${1:object}\n"
- "\tproperty: \"${2:name}\"\n"
- "\tduration: ${3:200}\n"
- "\teasing.type: "_ba.append(prefixWithDotForSnippet)
- .append("Easing.${4:InOutQuad}\n"
- "}"));
- result = makeSnippet(prefixForSnippet, "NumberAnimation with targets snippet",
- "NumberAnimation {\n"
- "\ttargets: [${1:object}]\n"
- "\tproperties: \"${2:name}\"\n"
- "\tduration: ${3:200}\n"
- "}");
- result = makeSnippet(prefixForSnippet, "PauseAnimation snippet",
- "PauseAnimation {\n"
- "\tduration: ${1:200}\n"
- "}");
- result = makeSnippet(prefixForSnippet, "PropertyAction snippet",
- "PropertyAction {\n"
- "\ttarget: ${1:object}\n"
- "\tproperty: \"${2:name}\"\n"
- "}");
- result = makeSnippet(prefixForSnippet, "PropertyAction with targets snippet",
- "PropertyAction {\n"
- "\ttargets: [${1:object}]\n"
- "\tproperties: \"${2:name}\"\n"
- "}");
- result = makeSnippet(prefixForSnippet, "PropertyChanges snippet",
- "PropertyChanges {\n"
- "\ttarget: ${1:object}\n"
- "}");
- result = makeSnippet(prefixForSnippet, "State snippet",
- "State {\n"
- "\tname: ${1:name}\n"
- "\t"_ba.append(prefixWithDotForSnippet)
- .append("PropertyChanges {\n"
- "\t\ttarget: ${2:object}\n"
- "\t}\n"
- "}"));
- result = makeSnippet(prefixForSnippet, "Text snippet",
- "Text {\n"
- "\tid: ${1:name}\n"
- "\ttext: qsTr(\"${2:text}\")\n"
- "}");
- result = makeSnippet(prefixForSnippet, "Transition snippet",
- "Transition {\n"
- "\tfrom: \"${1:fromState}\"\n"
- "\tto: \"${2:toState}\"\n"
- "}");
-
- if (!userTypedQualifier.isEmpty())
- return;
-
- auto resolver = file->typeResolver();
- if (!resolver)
- return;
- const auto qquickItemScope = resolver->typeForName(prefixWithDotForSnippet + u"Item"_s);
- const QQmlJSScope::ConstPtr ownerScope = itemAtPosition.qmlObject().semanticScope();
- if (!ownerScope || !qquickItemScope)
- return;
-
- if (ownerScope->inherits(qquickItemScope)) {
- result = makeSnippet("states binding with PropertyChanges in State",
- "states: [\n"
- "\t"_ba.append(prefixWithDotForSnippet)
- .append("State {\n"
- "\t\tname: \"${1:name}\"\n"
- "\t\t"_ba.append(prefixWithDotForSnippet)
- .append("PropertyChanges {\n"
- "\t\t\ttarget: ${2:object}\n"
- "\t\t}\n"
- "\t}\n"
- "]")));
- result = makeSnippet("transitions binding with Transition",
- "transitions: [\n"
- "\t"_ba.append(prefixWithDotForSnippet)
- .append("Transition {\n"
- "\t\tfrom: \"${1:fromState}\"\n"
- "\t\tto: \"${2:fromState}\"\n"
- "\t}\n"
- "]"));
- }
-}
-
/*!
\internal
Decide which completions can be used at currentItem and compute them.
@@ -1977,4 +1839,50 @@ void QQmlLSCompletion::collectCompletions(const DomItem &currentItem,
return;
}
+QQmlLSCompletion::QQmlLSCompletion(const QFactoryLoader &pluginLoader)
+{
+ const auto keys = pluginLoader.metaDataKeys();
+ for (qsizetype i = 0; i < keys.size(); ++i) {
+ auto instance = std::unique_ptr<QQmlLSPlugin>(
+ qobject_cast<QQmlLSPlugin *>(pluginLoader.instance(i)));
+ if (!instance)
+ continue;
+ if (auto completionInstance = instance->createCompletionPlugin())
+ m_plugins.push_back(std::move(completionInstance));
+ }
+}
+
+/*!
+\internal
+Helper method to call a method on all loaded plugins.
+*/
+void QQmlLSCompletion::collectFromPlugins(qxp::function_ref<CompletionFromPluginFunction> f,
+ BackInsertIterator result) const
+{
+ for (const auto &plugin : m_plugins) {
+ Q_ASSERT(plugin);
+ f(plugin.get(), result);
+ }
+}
+
+void QQmlLSCompletion::suggestSnippetsForLeftHandSideOfBinding(const DomItem &itemAtPosition,
+ BackInsertIterator result) const
+{
+ collectFromPlugins(
+ [&itemAtPosition](QQmlLSCompletionPlugin *p, BackInsertIterator result) {
+ p->suggestSnippetsForLeftHandSideOfBinding(itemAtPosition, result);
+ },
+ result);
+}
+
+void QQmlLSCompletion::suggestSnippetsForRightHandSideOfBinding(const DomItem &itemAtPosition,
+ BackInsertIterator result) const
+{
+ collectFromPlugins(
+ [&itemAtPosition](QQmlLSCompletionPlugin *p, BackInsertIterator result) {
+ p->suggestSnippetsForRightHandSideOfBinding(itemAtPosition, result);
+ },
+ result);
+}
+
QT_END_NAMESPACE
diff --git a/src/qmlls/qqmllscompletion_p.h b/src/qmlls/qqmllscompletion_p.h
index 2831a97ce2..18e0d19059 100644
--- a/src/qmlls/qqmllscompletion_p.h
+++ b/src/qmlls/qqmllscompletion_p.h
@@ -17,11 +17,15 @@
#include "qqmlcompletioncontextstrings_p.h"
#include "qqmllsutils_p.h"
+#include "qqmllsplugin_p.h"
#include <QtLanguageServer/private/qlanguageserverspectypes_p.h>
#include <QtQmlDom/private/qqmldomexternalitems_p.h>
#include <QtQmlDom/private/qqmldomtop_p.h>
#include <QtCore/private/qduplicatetracker_p.h>
+#include <QtCore/private/qfactoryloader_p.h>
+#include <QtCore/qpluginloader.h>
+#include <QtCore/qxpfunctional.h>
QT_BEGIN_NAMESPACE
@@ -29,12 +33,11 @@ Q_DECLARE_LOGGING_CATEGORY(QQmlLSCompletionLog)
enum QQmlLSUtilsAppendOption { AppendSemicolon, AppendNothing };
-
class QQmlLSCompletion
{
using DomItem = QQmlJS::Dom::DomItem;
public:
- // TODO: constructor with build paths to load plugins
+ QQmlLSCompletion(const QFactoryLoader &pluginLoader);
using CompletionItem = QLspSpecification::CompletionItem;
using BackInsertIterator = std::back_insert_iterator<QList<CompletionItem>>;
@@ -201,8 +204,24 @@ private:
QDuplicateTracker<QString> *usedNames,
BackInsertIterator it) const;
- // TODO: split + move to plugin
+ // TODO: split implementation into suggestSnippetsFor{Left,Right}HandSideOfBinding + move to plugin
void suggestQuickSnippetsCompletion(const DomItem &itemAtPosition, BackInsertIterator it) const;
+
+ void suggestSnippetsForLeftHandSideOfBinding(const DomItem &items,
+ BackInsertIterator result) const;
+
+ void suggestSnippetsForRightHandSideOfBinding(const DomItem &items,
+ BackInsertIterator result) const;
+
+private:
+ using CompletionFromPluginFunction = void(QQmlLSCompletionPlugin *plugin,
+ BackInsertIterator result);
+ void collectFromPlugins(const qxp::function_ref<CompletionFromPluginFunction> f,
+ BackInsertIterator result) const;
+
+ QStringList m_loadPaths;
+
+ std::vector<std::unique_ptr<QQmlLSCompletionPlugin>> m_plugins;
};
QT_END_NAMESPACE
diff --git a/src/qmlls/qqmllscompletionplugin.cpp b/src/qmlls/qqmllscompletionplugin.cpp
new file mode 100644
index 0000000000..fd47c691f7
--- /dev/null
+++ b/src/qmlls/qqmllscompletionplugin.cpp
@@ -0,0 +1,4 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qqmllscompletionplugin_p.h"
diff --git a/src/qmlls/qqmllscompletionplugin_p.h b/src/qmlls/qqmllscompletionplugin_p.h
new file mode 100644
index 0000000000..0dde7bec76
--- /dev/null
+++ b/src/qmlls/qqmllscompletionplugin_p.h
@@ -0,0 +1,42 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQMLLSCOMPLETIONPLUGIN_H
+#define QQMLLSCOMPLETIONPLUGIN_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <iterator>
+
+#include <QtQmlDom/private/qqmldomelements_p.h>
+#include <QtLanguageServer/private/qlanguageserverspectypes_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQmlLSCompletionPlugin
+{
+public:
+ QQmlLSCompletionPlugin() = default;
+ virtual ~QQmlLSCompletionPlugin() = default;
+
+ using BackInsertIterator = std::back_insert_iterator<QList<QLspSpecification::CompletionItem>>;
+
+ virtual void suggestSnippetsForLeftHandSideOfBinding(const QQmlJS::Dom::DomItem &items,
+ BackInsertIterator result) const = 0;
+
+ virtual void suggestSnippetsForRightHandSideOfBinding(const QQmlJS::Dom::DomItem &items,
+ BackInsertIterator result) const = 0;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQMLLSCOMPLETIONPLUGIN_H
diff --git a/src/qmlls/qqmllsplugin_p.h b/src/qmlls/qqmllsplugin_p.h
new file mode 100644
index 0000000000..07699ce2c5
--- /dev/null
+++ b/src/qmlls/qqmllsplugin_p.h
@@ -0,0 +1,42 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QMLLSPLUGIN_P_H
+#define QMLLSPLUGIN_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <memory>
+
+#include <QtCore/qtclasshelpermacros.h>
+#include <QtCore/qobject.h>
+#include <QtQmlLS/private/qqmllscompletionplugin_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQmlLSPlugin
+{
+public:
+ QQmlLSPlugin() = default;
+ virtual ~QQmlLSPlugin() = default;
+
+ Q_DISABLE_COPY_MOVE(QQmlLSPlugin)
+
+ virtual std::unique_ptr<QQmlLSCompletionPlugin> createCompletionPlugin() const = 0;
+};
+
+#define QmlLSPluginInterface_iid "org.qt-project.Qt.QmlLS.Plugin/1.0"
+Q_DECLARE_INTERFACE(QQmlLSPlugin, QmlLSPluginInterface_iid)
+
+QT_END_NAMESPACE
+
+#endif // QMLLSPLUGIN_P_H
diff --git a/tests/auto/qmlls/modules/tst_qmlls_modules.cpp b/tests/auto/qmlls/modules/tst_qmlls_modules.cpp
index 6fd810fff9..6010a1adf0 100644
--- a/tests/auto/qmlls/modules/tst_qmlls_modules.cpp
+++ b/tests/auto/qmlls/modules/tst_qmlls_modules.cpp
@@ -420,6 +420,22 @@ void tst_qmlls_modules::automaticSemicolonInsertionForCompletions()
QStringList({ u"bad"_s, u"BuildDirType"_s, u"QtQuick"_s, u"width"_s, u"vector4d"_s })));
}
+void tst_qmlls_modules::checkQuickSnippets()
+{
+ ignoreDiagnostics();
+ const auto uri = openFile(u"completions/Yyy.qml"_s);
+ QVERIFY(uri);
+
+ // if at least one snippet is there, then the pluginloading works. To test the plugin itself,
+ // add tests in tst_qmlls_utils instead.
+ QTEST_CHECKED(checkCompletions(
+ *uri, 4, 3,
+ ExpectedCompletions({
+ { u"BorderImage snippet"_s, CompletionItemKind::Snippet },
+ }),
+ QStringList({})));
+}
+
void tst_qmlls_modules::goToTypeDefinition_data()
{
QTest::addColumn<QString>("filePath");
diff --git a/tests/auto/qmlls/modules/tst_qmlls_modules.h b/tests/auto/qmlls/modules/tst_qmlls_modules.h
index 265afd601b..352af1d928 100644
--- a/tests/auto/qmlls/modules/tst_qmlls_modules.h
+++ b/tests/auto/qmlls/modules/tst_qmlls_modules.h
@@ -66,6 +66,7 @@ private slots:
void automaticSemicolonInsertionForCompletions();
void hover_data();
void hover();
+ void checkQuickSnippets();
private:
QProcess m_server;
diff --git a/tests/auto/qmlls/utils/tst_qmlls_utils.cpp b/tests/auto/qmlls/utils/tst_qmlls_utils.cpp
index c7906635ab..0bb0fbb114 100644
--- a/tests/auto/qmlls/utils/tst_qmlls_utils.cpp
+++ b/tests/auto/qmlls/utils/tst_qmlls_utils.cpp
@@ -3525,7 +3525,7 @@ void tst_qmlls_utils::completions()
qsizetype pos = QQmlLSUtils::textOffsetFrom(code, line - 1, character - 1);
CompletionContextStrings ctxt{ code, pos };
- QQmlLSCompletion completionEngine;
+ QQmlLSCompletion completionEngine(m_pluginLoader);
QList<CompletionItem> completions =
completionEngine.completions(locations.front().domItem, ctxt);
diff --git a/tests/auto/qmlls/utils/tst_qmlls_utils.h b/tests/auto/qmlls/utils/tst_qmlls_utils.h
index d7b1d70a13..166fe84a42 100644
--- a/tests/auto/qmlls/utils/tst_qmlls_utils.h
+++ b/tests/auto/qmlls/utils/tst_qmlls_utils.h
@@ -7,6 +7,7 @@
#include <QtJsonRpc/private/qjsonrpcprotocol_p.h>
#include <QtLanguageServer/private/qlanguageserverprotocol_p.h>
#include <QtQuickTestUtils/private/qmlutils_p.h>
+#include <QtCore/private/qfactoryloader_p.h>
#include <QtCore/qobject.h>
#include <QtCore/qprocess.h>
@@ -37,7 +38,11 @@ class tst_qmlls_utils : public QQmlDataTest
using ExpectedDocumentations = QList<ExpectedDocumentation>;
public:
- tst_qmlls_utils() : QQmlDataTest(QT_QMLLS_UTILS_DATADIR) { }
+ tst_qmlls_utils()
+ : QQmlDataTest(QT_QMLLS_UTILS_DATADIR),
+ m_pluginLoader(QmlLSPluginInterface_iid, u"/qmlls"_s)
+ {
+ }
private slots:
void textOffsetRowColumnConversions_data();
@@ -83,6 +88,7 @@ private:
using CacheKey = QString;
// avoid loading the same file over and over when running all the tests
QHash<CacheKey, EnvironmentAndFile> cache;
+ QFactoryLoader m_pluginLoader;
};
diff --git a/tools/qmlls/CMakeLists.txt b/tools/qmlls/CMakeLists.txt
index 8b5636879d..d61a18582e 100644
--- a/tools/qmlls/CMakeLists.txt
+++ b/tools/qmlls/CMakeLists.txt
@@ -16,3 +16,8 @@ qt_internal_add_app(qmlls
Qt::QmlToolingSettingsPrivate
)
set_target_properties(qmlls PROPERTIES WIN32_EXECUTABLE FALSE)
+
+if(NOT QT6_IS_SHARED_LIBS_BUILD)
+ qt_import_plugins(qmlls INCLUDE Qt::QmlLSQuickPlugin)
+ target_link_libraries(qmlls PRIVATE Qt::QmlLSQuickPlugin)
+endif()