Warning, file /kdevelop/kdevelop/plugins/switchtobuddy/switchtobuddyplugin.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002     SPDX-FileCopyrightText: 2012 André Stein <andre.stein@rwth-aachen.de>
0003     SPDX-FileCopyrightText: 2014 Kevin Funk <kfunk@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "switchtobuddyplugin.h"
0009 
0010 #include <KLocalizedString>
0011 #include <KPluginFactory>
0012 #include <KActionCollection>
0013 #include <QAction>
0014 #include <QFile>
0015 #include <QMimeDatabase>
0016 #include <QMimeType>
0017 
0018 #include <interfaces/icore.h>
0019 #include <interfaces/idocumentcontroller.h>
0020 #include <interfaces/ibuddydocumentfinder.h>
0021 #include <interfaces/contextmenuextension.h>
0022 #include <language/interfaces/editorcontext.h>
0023 #include <language/duchain/duchainutils.h>
0024 #include <language/duchain/functiondefinition.h>
0025 #include <language/duchain/parsingenvironment.h>
0026 #include <language/duchain/classfunctiondeclaration.h>
0027 #include <debug.h>
0028 
0029 #include <KTextEditor/Document>
0030 #include <KTextEditor/View>
0031 
0032 using namespace KDevelop;
0033 
0034 namespace {
0035 
0036 KTextEditor::Cursor normalizeCursor(KTextEditor::Cursor c)
0037 {
0038     c.setColumn(0);
0039     return c;
0040 }
0041 
0042 ///Tries to find a definition for the declaration at given cursor-position and document-url. DUChain must be locked.
0043 Declaration* definitionForCursorDeclaration(const KTextEditor::Cursor& cursor, const QUrl& url)
0044 {
0045     const QList<TopDUContext*> topContexts = DUChain::self()->chainsForDocument(url);
0046     for (TopDUContext* ctx : topContexts) {
0047         Declaration* decl = DUChainUtils::declarationInLine(cursor, ctx);
0048         if (!decl) {
0049             continue;
0050         }
0051         if (auto definition = FunctionDefinition::definition(decl)) {
0052             return definition;
0053         }
0054     }
0055     return nullptr;
0056 }
0057 
0058 QString findSwitchCandidate(const QUrl& docUrl)
0059 {
0060     QMimeDatabase db;
0061     IBuddyDocumentFinder* finder = IBuddyDocumentFinder::finderForMimeType(db.mimeTypeForUrl(docUrl).name());
0062     if (finder) {
0063         // get the first entry that exists, use that as candidate
0064         const auto potentialBuddies = finder->potentialBuddies(docUrl);
0065         for (const QUrl& buddyUrl : potentialBuddies) {
0066             if (!QFile::exists(buddyUrl.toLocalFile())) {
0067                 continue;
0068             }
0069 
0070             return buddyUrl.toLocalFile();
0071         }
0072     }
0073     return QString();
0074 }
0075 
0076 }
0077 
0078 K_PLUGIN_FACTORY_WITH_JSON(SwitchToBuddyPluginFactory, "kdevswitchtobuddy.json", registerPlugin<SwitchToBuddyPlugin>(); )
0079 
0080 SwitchToBuddyPlugin::SwitchToBuddyPlugin ( QObject* parent, const QVariantList& )
0081     : IPlugin ( QStringLiteral("kdevswitchtobuddy"), parent )
0082 {
0083     setXMLFile(QStringLiteral("kdevswitchtobuddy.rc"));
0084 }
0085 
0086 SwitchToBuddyPlugin::~SwitchToBuddyPlugin()
0087 {
0088 }
0089 
0090 ContextMenuExtension SwitchToBuddyPlugin::contextMenuExtension(Context* context, QWidget* parent)
0091 {
0092     auto* ctx = dynamic_cast<EditorContext*>(context);
0093     if (!ctx) {
0094         return ContextMenuExtension();
0095     }
0096 
0097     QUrl currentUrl = ctx->url();
0098     IBuddyDocumentFinder* buddyFinder = IBuddyDocumentFinder::finderForMimeType(QMimeDatabase().mimeTypeForUrl(currentUrl).name());
0099     if (!buddyFinder)
0100         return ContextMenuExtension();
0101 
0102     // Get all potential buddies for the current document and add a switch-to action
0103     // for each buddy who really exists in the file system. Note: if no buddies could be calculated
0104     // no extension actions are generated.
0105     const QVector<QUrl>& potentialBuddies = buddyFinder->potentialBuddies(currentUrl);
0106 
0107     ContextMenuExtension extension;
0108 
0109     for (const QUrl& url : potentialBuddies) {
0110         if (!QFile::exists(url.toLocalFile())) {
0111             continue;
0112         }
0113 
0114         auto* action = new QAction(i18nc("@action:inmenu", "Switch to '%1'", url.fileName()), parent);
0115         const QString surl = url.toLocalFile();
0116         connect(action, &QAction::triggered, this, [this, surl](){ switchToBuddy(surl); }, Qt::QueuedConnection);
0117         extension.addAction(ContextMenuExtension::NavigationGroup, action);
0118     }
0119 
0120     return extension;
0121 }
0122 
0123 void SwitchToBuddyPlugin::createActionsForMainWindow(Sublime::MainWindow* /*window*/, QString& xmlFile, KActionCollection& actions)
0124 {
0125     xmlFile = this->xmlFile();
0126 
0127     QAction* switchDefinitionDeclaration = actions.addAction(QStringLiteral("switch_definition_declaration"));
0128     switchDefinitionDeclaration->setText(i18nc("@action", "&Switch Definition/Declaration"));
0129     actions.setDefaultShortcut(switchDefinitionDeclaration, Qt::CTRL | Qt::SHIFT | Qt::Key_C);
0130     connect(switchDefinitionDeclaration, &QAction::triggered, this, &SwitchToBuddyPlugin::switchDefinitionDeclaration);
0131 
0132     QAction* switchHeaderSource = actions.addAction(QStringLiteral("switch_header_source"));
0133     switchHeaderSource->setText(i18nc("@action", "Switch Header/Source"));
0134     actions.setDefaultShortcut(switchHeaderSource, Qt::CTRL | Qt::Key_Slash);
0135     connect(switchHeaderSource, &QAction::triggered, this, &SwitchToBuddyPlugin::switchHeaderSource);
0136 }
0137 
0138 void SwitchToBuddyPlugin::switchHeaderSource()
0139 {
0140     qCDebug(PLUGIN_SWITCHTOBUDDY) << "switching header/source";
0141 
0142     auto doc = ICore::self()->documentController()->activeDocument();
0143     if (!doc)
0144         return;
0145 
0146     QString buddyUrl = findSwitchCandidate(doc->url());
0147     if (!buddyUrl.isEmpty())
0148         switchToBuddy(buddyUrl);
0149 }
0150 
0151 void SwitchToBuddyPlugin::switchToBuddy(const QString& url)
0152 {
0153     KDevelop::ICore::self()->documentController()->openDocument(QUrl::fromLocalFile(url));
0154 }
0155 
0156 void SwitchToBuddyPlugin::switchDefinitionDeclaration()
0157 {
0158     qCDebug(PLUGIN_SWITCHTOBUDDY) << "switching definition/declaration";
0159 
0160     QUrl docUrl;
0161     KTextEditor::Cursor cursor;
0162 
0163     ///Step 1: Find the current top-level context of type DUContext::Other(the highest code-context).
0164     ///-- If it belongs to a function-declaration or definition, it can be retrieved through owner(), and we are in a definition.
0165     ///-- If no such context could be found, search for a declaration on the same line as the cursor, and switch to the according definition
0166     {
0167         auto view = ICore::self()->documentController()->activeTextDocumentView();
0168         if (!view) {
0169             qCDebug(PLUGIN_SWITCHTOBUDDY) << "No active document";
0170             return;
0171         }
0172 
0173         docUrl = view->document()->url();
0174         cursor = view->cursorPosition();
0175     }
0176 
0177     QString switchCandidate = findSwitchCandidate(docUrl);
0178     if(!switchCandidate.isEmpty()) {
0179 
0180         DUChainReadLocker lock;
0181 
0182         //If the file has not been parsed yet, update it
0183         TopDUContext* ctx = DUChainUtils::standardContextForUrl(docUrl);
0184         //At least 'VisibleDeclarationsAndContexts' is required so we can do a switch
0185         if (!ctx || (ctx->parsingEnvironmentFile() && !ctx->parsingEnvironmentFile()->featuresSatisfied(TopDUContext::AllDeclarationsContextsAndUses))) {
0186             lock.unlock();
0187             qCDebug(PLUGIN_SWITCHTOBUDDY) << "Parsing switch-candidate before switching" << switchCandidate;
0188             ReferencedTopDUContext updatedContext = DUChain::self()->waitForUpdate(IndexedString(switchCandidate), TopDUContext::AllDeclarationsContextsAndUses);
0189             if (!updatedContext) {
0190                 qCDebug(PLUGIN_SWITCHTOBUDDY) << "Failed to update document:" << switchCandidate;
0191                 return;
0192             }
0193         }
0194     }
0195 
0196     qCDebug(PLUGIN_SWITCHTOBUDDY) << "Document:" << docUrl;
0197 
0198     DUChainReadLocker lock;
0199 
0200     TopDUContext* standardCtx = DUChainUtils::standardContextForUrl(docUrl);
0201 
0202     bool wasSignal = false;
0203     if (standardCtx) {
0204         Declaration* definition = nullptr;
0205 
0206         DUContext* ctx = standardCtx->findContext(standardCtx->transformToLocalRevision(cursor));
0207         if (!ctx) {
0208             ctx = standardCtx;
0209         }
0210 
0211         while (ctx && ctx->parentContext() && (ctx->parentContext()->type() == DUContext::Other || ctx->parentContext()->type() == DUContext::Function)) {
0212             ctx = ctx->parentContext();
0213         }
0214 
0215         if (ctx && ctx->owner() && (ctx->type() == DUContext::Other || ctx->type() == DUContext::Function) && ctx->owner()->isDefinition()) {
0216             definition = ctx->owner();
0217             qCDebug(PLUGIN_SWITCHTOBUDDY) << "found definition while traversing:" << definition->toString();
0218         }
0219 
0220         if (!definition && ctx) {
0221             definition = DUChainUtils::declarationInLine(cursor, ctx);
0222         }
0223 
0224         if (auto* cDef = dynamic_cast<ClassFunctionDeclaration*>(definition)) {
0225             if (cDef->isSignal()) {
0226                 qCDebug(PLUGIN_SWITCHTOBUDDY) << "found definition is a signal, not switching to .moc implementation";
0227                 definition = nullptr;
0228                 wasSignal = true;
0229             }
0230         }
0231 
0232         auto* def = dynamic_cast<FunctionDefinition*>(definition);
0233         if (def && def->declaration()) {
0234             Declaration* declaration = def->declaration();
0235             KTextEditor::Range targetRange = declaration->rangeInCurrentRevision();
0236             const auto url = declaration->url().toUrl();
0237             qCDebug(PLUGIN_SWITCHTOBUDDY) << "found definition that has declaration: " << definition->toString() << "range" << targetRange << "url" << url;
0238             lock.unlock();
0239 
0240             auto view = ICore::self()->documentController()->activeTextDocumentView();
0241             if (view && !targetRange.contains(view->cursorPosition())) {
0242                 const auto pos = normalizeCursor(targetRange.start());
0243                 ICore::self()->documentController()->openDocument(url, KTextEditor::Range(pos, pos));
0244             } else {
0245                 ICore::self()->documentController()->openDocument(url);
0246             }
0247             return;
0248         } else {
0249             qCDebug(PLUGIN_SWITCHTOBUDDY) << "Definition has no assigned declaration";
0250         }
0251 
0252         qCDebug(PLUGIN_SWITCHTOBUDDY) << "Could not get definition/declaration from context";
0253     } else {
0254         qCDebug(PLUGIN_SWITCHTOBUDDY) << "Got no context for the current document";
0255     }
0256 
0257     Declaration* def = nullptr;
0258     if (!wasSignal) {
0259         def = definitionForCursorDeclaration(cursor, docUrl);
0260     }
0261 
0262     if (def) {
0263         const auto url = def->url().toUrl();
0264         KTextEditor::Range targetRange = def->rangeInCurrentRevision();
0265 
0266         if (def->internalContext()) {
0267             targetRange.end() = def->internalContext()->rangeInCurrentRevision().end();
0268         } else {
0269             qCDebug(PLUGIN_SWITCHTOBUDDY) << "Declaration does not have internal context";
0270         }
0271         lock.unlock();
0272 
0273         auto view = ICore::self()->documentController()->activeTextDocumentView();
0274         if (view && !targetRange.contains(view->cursorPosition())) {
0275             KTextEditor::Cursor pos(normalizeCursor(targetRange.start()));
0276             ICore::self()->documentController()->openDocument(url, KTextEditor::Range(pos, pos));
0277         } else {
0278             //The cursor is already in the target range, only open the document
0279             ICore::self()->documentController()->openDocument(url);
0280         }
0281         return;
0282     } else if (!wasSignal) {
0283         qCWarning(PLUGIN_SWITCHTOBUDDY) << "Found no definition assigned to cursor position";
0284     }
0285 
0286     lock.unlock();
0287 
0288     ///- If no definition/declaration could be found to switch to, just switch the document using normal header/source heuristic by file-extension
0289     if (!switchCandidate.isEmpty()) {
0290         ICore::self()->documentController()->openDocument(QUrl::fromUserInput(switchCandidate));
0291     } else {
0292         qCDebug(PLUGIN_SWITCHTOBUDDY) << "Found no source/header candidate to switch";
0293     }
0294 }
0295 
0296 #include "switchtobuddyplugin.moc"
0297 #include "moc_switchtobuddyplugin.cpp"