File indexing completed on 2024-05-05 04:40:49

0001 /*
0002     SPDX-FileCopyrightText: 2012 Aleix Pol <aleixpol@kde.org>
0003     SPDX-FileCopyrightText: 2012 Milian Wolff <mail@milianw.de>
0004     SPDX-FileCopyrightText: 2013 Sven Brauch <svenbrauch@gmail.com>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "kdevqmljsplugin.h"
0010 
0011 #include "qmljsparsejob.h"
0012 #include "qmljshighlighting.h"
0013 #include "codecompletion/model.h"
0014 #include "navigation/propertypreviewwidget.h"
0015 #include "duchain/helper.h"
0016 
0017 #include <qmljs/qmljsmodelmanagerinterface.h>
0018 
0019 #include <KPluginFactory>
0020 
0021 #include <language/codecompletion/codecompletion.h>
0022 #include <language/assistant/renameassistant.h>
0023 #include <language/assistant/staticassistantsmanager.h>
0024 #include <language/codegen/basicrefactoring.h>
0025 #include <language/duchain/duchain.h>
0026 #include <language/duchain/duchainutils.h>
0027 #include <language/interfaces/editorcontext.h>
0028 #include <interfaces/icore.h>
0029 #include <interfaces/idocumentcontroller.h>
0030 #include <interfaces/ilanguagecontroller.h>
0031 #include <interfaces/contextmenuextension.h>
0032 
0033 #include <QReadWriteLock>
0034 
0035 K_PLUGIN_FACTORY_WITH_JSON(KDevQmlJsSupportFactory, "kdevqmljs.json", registerPlugin<KDevQmlJsPlugin>(); )
0036 
0037 using namespace KDevelop;
0038 
0039 /// TODO: Extend? See qmljsmodelmanager.h in qt-creator.git
0040 class ModelManager: public QmlJS::ModelManagerInterface
0041 {
0042     Q_OBJECT
0043 
0044 public:
0045     explicit ModelManager(QObject *parent = nullptr);
0046     ~ModelManager() override;
0047 };
0048 
0049 ModelManager::ModelManager(QObject* parent)
0050     : QmlJS::ModelManagerInterface(parent) {}
0051 
0052 ModelManager::~ModelManager() {}
0053 
0054 KDevQmlJsPlugin::KDevQmlJsPlugin(QObject* parent, const QVariantList& )
0055 : IPlugin(QStringLiteral("kdevqmljssupport"), parent )
0056 , ILanguageSupport()
0057 , m_highlighting(new QmlJsHighlighting(this))
0058 , m_refactoring(new BasicRefactoring(this))
0059 , m_modelManager(new ModelManager(this))
0060 {
0061     QmlJS::registerDUChainItems();
0062 
0063     CodeCompletionModel* codeCompletion = new QmlJS::CodeCompletionModel(this);
0064     new KDevelop::CodeCompletion(this, codeCompletion, name());
0065 
0066     auto assistantsManager = core()->languageController()->staticAssistantsManager();
0067     assistantsManager->registerAssistant(StaticAssistant::Ptr(new RenameAssistant(this)));
0068 }
0069 
0070 KDevQmlJsPlugin::~KDevQmlJsPlugin()
0071 {
0072     parseLock()->lockForWrite();
0073     // By locking the parse-mutexes, we make sure that parse jobs get a chance to finish in a good state
0074     parseLock()->unlock();
0075 
0076     QmlJS::unregisterDUChainItems();
0077 }
0078 
0079 ParseJob* KDevQmlJsPlugin::createParseJob(const IndexedString& url)
0080 {
0081     return new QmlJsParseJob(url, this);
0082 }
0083 
0084 QString KDevQmlJsPlugin::name() const
0085 {
0086     return QStringLiteral("qml/js");
0087 }
0088 
0089 ICodeHighlighting* KDevQmlJsPlugin::codeHighlighting() const
0090 {
0091     return m_highlighting;
0092 }
0093 
0094 BasicRefactoring* KDevQmlJsPlugin::refactoring() const
0095 {
0096     return m_refactoring;
0097 }
0098 
0099 ContextMenuExtension KDevQmlJsPlugin::contextMenuExtension(Context* context, QWidget* parent)
0100 {
0101     ContextMenuExtension cm;
0102     auto *ec = dynamic_cast<KDevelop::EditorContext *>(context);
0103 
0104     if (ec && ICore::self()->languageController()->languagesForUrl(ec->url()).contains(this)) {
0105         // It's a QML/JS file, let's add our context menu.
0106         m_refactoring->fillContextMenu(cm, context, parent);
0107     }
0108 
0109     return cm;
0110 }
0111 
0112 const QString textFromDoc(const IDocument* doc, const KTextEditor::Range& range) {
0113     return doc->textDocument()->line(range.start().line()).mid(range.start().column(), range.end().column()-range.start().column());
0114 }
0115 
0116 // Finds how many spaces the given string has at one end.
0117 // direction=+1 -> left end of the string, -1 for right end.
0118 int spacesAtCorner(const QString& string, int direction = +1) {
0119     int spaces = 0;
0120     QString::const_iterator it;
0121     for ( it = direction == 1 ? string.begin() : string.end()-1 ; it != string.end(); it += direction ) {
0122         if ( ! it->isSpace() ) break;
0123         spaces += 1;
0124     }
0125     return spaces;
0126 }
0127 
0128 // Take the given QML line and check if it's a line of the form foo.bar: value.
0129 // Return ranges for the key and the value.
0130 const QPair<KTextEditor::Range, KTextEditor::Range> parseProperty(const QString& line, const KTextEditor::Cursor& position) {
0131     const QStringList items = line.split(QLatin1Char(';'));
0132     QString matchingItem;
0133     int col_offset = -1;
0134     // This is to also support FooAnimation { foo: bar; baz: bang; duration: 200 }
0135     // or similar
0136     for (const QString& item : items) {
0137         col_offset += item.size() + 1;
0138         if ( position.column() < col_offset ) {
0139             matchingItem = item;
0140             break;
0141         }
0142     }
0143     QStringList split = matchingItem.split(QLatin1Char(':'));
0144     if ( split.size() != 2 ) {
0145         // The expression is not of the form foo:bar, thus invalid.
0146         return qMakePair(KTextEditor::Range::invalid(), KTextEditor::Range::invalid());
0147     }
0148     QString key = split.at(0);
0149     QString value = split.at(1);
0150 
0151     // For animations or similar, remove the trailing '}' if there's no semicolon after the last entry
0152     if (value.trimmed().endsWith(QLatin1Char('}'))) {
0153         col_offset -= value.size() - value.lastIndexOf(QLatin1Char('}')) + 1;
0154         value = value.left(value.lastIndexOf(QLatin1Char('}'))-1);
0155     }
0156 
0157     return qMakePair(
0158     KTextEditor::Range(
0159         KTextEditor::Cursor(position.line(), col_offset - value.size() - key.size() + spacesAtCorner(key, +1) - 1),
0160         KTextEditor::Cursor(position.line(), col_offset - value.size() - 1 + spacesAtCorner(key, -1))
0161     ),
0162     KTextEditor::Range(
0163         KTextEditor::Cursor(position.line(), col_offset - value.size() + spacesAtCorner(value, +1)),
0164         KTextEditor::Cursor(position.line(), col_offset + spacesAtCorner(value, -1))
0165     ));
0166 }
0167 
0168 QPair<QWidget*, KTextEditor::Range> KDevQmlJsPlugin::specialLanguageObjectNavigationWidget(const QUrl& url, const KTextEditor::Cursor& position)
0169 {
0170     IDocument* doc = ICore::self()->documentController()->documentForUrl(url);
0171     if ( doc && doc->textDocument() ) {
0172         // Check for a QML property, and construct a property preview widget
0173         // if the property key is listed in the supported properties.
0174         QPair<KTextEditor::Range, KTextEditor::Range> property = parseProperty(doc->textDocument()->line(position.line()), position);
0175         if ( property.first.isValid() && property.second.isValid() ) {
0176             const auto itemUnderCursor = DUChainUtils::itemUnderCursor(url, property.first.start());
0177 
0178             return {PropertyPreviewWidget::constructIfPossible(
0179                 doc->textDocument(),
0180                 property.first,
0181                 property.second,
0182                 itemUnderCursor.declaration,
0183                 textFromDoc(doc, property.first),
0184                 textFromDoc(doc, property.second)
0185             ), itemUnderCursor.range};
0186         }
0187     }
0188     // Otherwise, display no special "navigation" widget.
0189     return KDevelop::ILanguageSupport::specialLanguageObjectNavigationWidget(url, position);
0190 }
0191 
0192 #include "kdevqmljsplugin.moc"
0193 #include "moc_kdevqmljsplugin.cpp"