diff --git a/kdevplatform/language/CMakeLists.txt b/kdevplatform/language/CMakeLists.txt index 685b6cc5f2..8cadf4b734 100644 --- a/kdevplatform/language/CMakeLists.txt +++ b/kdevplatform/language/CMakeLists.txt @@ -1,403 +1,404 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevplatform\") # Check whether malloc_trim(3) is supported. include(CheckIncludeFile) include(CheckSymbolExists) check_include_file("malloc.h" HAVE_MALLOC_H) check_symbol_exists(malloc_trim "malloc.h" HAVE_MALLOC_TRIM) # TODO: fix duchain/stringhelpers.cpp and drop this again remove_definitions( -DQT_NO_CAST_FROM_ASCII ) if(BUILD_TESTING) add_subdirectory(highlighting/tests) add_subdirectory(duchain/tests) add_subdirectory(backgroundparser/tests) add_subdirectory(codegen/tests) add_subdirectory(util/tests) endif() configure_file(${CMAKE_CURRENT_SOURCE_DIR}/language-features.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/language-features.h ) set(KDevPlatformLanguage_LIB_SRCS assistant/staticassistantsmanager.cpp assistant/renameaction.cpp assistant/renameassistant.cpp assistant/renamefileaction.cpp assistant/staticassistant.cpp editor/persistentmovingrangeprivate.cpp editor/persistentmovingrange.cpp editor/modificationrevisionset.cpp editor/modificationrevision.cpp backgroundparser/backgroundparser.cpp backgroundparser/parsejob.cpp backgroundparser/documentchangetracker.cpp backgroundparser/parseprojectjob.cpp backgroundparser/urlparselock.cpp duchain/specializationstore.cpp duchain/codemodel.cpp duchain/duchain.cpp duchain/waitforupdate.cpp duchain/duchainpointer.cpp duchain/ducontext.cpp duchain/indexedducontext.cpp duchain/indexedtopducontext.cpp duchain/localindexedducontext.cpp duchain/indexeddeclaration.cpp duchain/localindexeddeclaration.cpp duchain/topducontext.cpp duchain/topducontextdynamicdata.cpp duchain/topducontextutils.cpp duchain/functiondefinition.cpp duchain/declaration.cpp duchain/classmemberdeclaration.cpp duchain/classfunctiondeclaration.cpp duchain/classdeclaration.cpp duchain/use.cpp duchain/forwarddeclaration.cpp duchain/duchainbase.cpp duchain/duchainlock.cpp duchain/identifier.cpp duchain/parsingenvironment.cpp duchain/abstractfunctiondeclaration.cpp duchain/functiondeclaration.cpp duchain/stringhelpers.cpp duchain/namespacealiasdeclaration.cpp duchain/aliasdeclaration.cpp duchain/dumpdotgraph.cpp duchain/duchainutils.cpp duchain/declarationid.cpp duchain/definitions.cpp duchain/uses.cpp duchain/importers.cpp duchain/duchaindumper.cpp duchain/duchainregister.cpp duchain/persistentsymboltable.cpp duchain/instantiationinformation.cpp duchain/problem.cpp duchain/types/typesystem.cpp duchain/types/typeregister.cpp duchain/types/typerepository.cpp duchain/types/identifiedtype.cpp duchain/types/abstracttype.cpp duchain/types/integraltype.cpp duchain/types/functiontype.cpp duchain/types/structuretype.cpp duchain/types/pointertype.cpp duchain/types/referencetype.cpp duchain/types/delayedtype.cpp duchain/types/arraytype.cpp duchain/types/indexedtype.cpp duchain/types/enumerationtype.cpp duchain/types/constantintegraltype.cpp duchain/types/enumeratortype.cpp duchain/types/typeutils.cpp duchain/types/typealiastype.cpp duchain/types/unsuretype.cpp duchain/types/containertypes.cpp duchain/builders/dynamiclanguageexpressionvisitor.cpp duchain/navigation/problemnavigationcontext.cpp duchain/navigation/abstractnavigationwidget.cpp duchain/navigation/abstractnavigationcontext.cpp duchain/navigation/usesnavigationcontext.cpp duchain/navigation/abstractdeclarationnavigationcontext.cpp duchain/navigation/abstractincludenavigationcontext.cpp duchain/navigation/useswidget.cpp duchain/navigation/usescollector.cpp + duchain/navigation/quickopenembeddedwidgetcombiner.cpp interfaces/abbreviations.cpp interfaces/iastcontainer.cpp interfaces/ilanguagesupport.cpp interfaces/quickopendataprovider.cpp interfaces/iquickopen.cpp interfaces/editorcontext.cpp interfaces/codecontext.cpp interfaces/icreateclasshelper.cpp interfaces/icontextbrowser.cpp codecompletion/codecompletion.cpp codecompletion/codecompletionworker.cpp codecompletion/codecompletionmodel.cpp codecompletion/codecompletionitem.cpp codecompletion/codecompletioncontext.cpp codecompletion/codecompletionitemgrouper.cpp codecompletion/codecompletionhelper.cpp codecompletion/normaldeclarationcompletionitem.cpp codegen/applychangeswidget.cpp codegen/coderepresentation.cpp codegen/documentchangeset.cpp codegen/duchainchangeset.cpp codegen/utilities.cpp codegen/codedescription.cpp codegen/basicrefactoring.cpp codegen/progressdialogs/refactoringdialog.cpp util/setrepository.cpp util/includeitem.cpp util/navigationtooltip.cpp highlighting/colorcache.cpp highlighting/configurablecolors.cpp highlighting/codehighlighting.cpp checks/dataaccessrepository.cpp checks/dataaccess.cpp checks/controlflowgraph.cpp checks/controlflownode.cpp classmodel/classmodel.cpp classmodel/classmodelnode.cpp classmodel/classmodelnodescontroller.cpp classmodel/allclassesfolder.cpp classmodel/documentclassesfolder.cpp classmodel/projectfolder.cpp codegen/templatesmodel.cpp codegen/templatepreviewicon.cpp codegen/templateclassgenerator.cpp codegen/sourcefiletemplate.cpp codegen/templaterenderer.cpp codegen/templateengine.cpp codegen/archivetemplateloader.cpp ) declare_qt_logging_category(KDevPlatformLanguage_LIB_SRCS TYPE LIBRARY CATEGORY_BASENAME "language" ) ki18n_wrap_ui(KDevPlatformLanguage_LIB_SRCS codegen/basicrefactoring.ui codegen/progressdialogs/refactoringdialog.ui) kdevplatform_add_library(KDevPlatformLanguage SOURCES ${KDevPlatformLanguage_LIB_SRCS}) target_include_directories(KDevPlatformLanguage PRIVATE ${Boost_INCLUDE_DIRS}) target_link_libraries(KDevPlatformLanguage PUBLIC KDev::Serialization KDev::Interfaces KDev::Util KF5::ThreadWeaver PRIVATE KDev::Project KF5::GuiAddons KF5::TextEditor KF5::Parts KF5::Archive KF5::IconThemes Grantlee5::Templates ) install(FILES assistant/renameaction.h assistant/renameassistant.h assistant/staticassistant.h assistant/staticassistantsmanager.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/assistant COMPONENT Devel ) install(FILES interfaces/ilanguagesupport.h interfaces/icodehighlighting.h interfaces/quickopendataprovider.h interfaces/quickopenfilter.h interfaces/iquickopen.h interfaces/codecontext.h interfaces/editorcontext.h interfaces/iastcontainer.h interfaces/icreateclasshelper.h interfaces/icontextbrowser.h interfaces/abbreviations.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/interfaces COMPONENT Devel ) install(FILES editor/persistentmovingrange.h editor/documentrange.h editor/documentcursor.h editor/cursorinrevision.h editor/rangeinrevision.h editor/modificationrevision.h editor/modificationrevisionset.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/editor COMPONENT Devel ) install(FILES backgroundparser/backgroundparser.h backgroundparser/parsejob.h backgroundparser/parseprojectjob.h backgroundparser/urlparselock.h backgroundparser/documentchangetracker.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/backgroundparser COMPONENT Devel ) install(FILES util/navigationtooltip.h util/setrepository.h util/basicsetrepository.h util/includeitem.h util/debuglanguageparserhelper.h util/kdevhash.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/util COMPONENT Devel ) install(FILES duchain/parsingenvironment.h duchain/duchain.h duchain/codemodel.h duchain/ducontext.h duchain/ducontextdata.h duchain/topducontext.h duchain/topducontextutils.h duchain/topducontextdata.h duchain/declaration.h duchain/declarationdata.h duchain/classmemberdeclaration.h duchain/classmemberdeclarationdata.h duchain/classfunctiondeclaration.h duchain/classdeclaration.h duchain/functiondefinition.h duchain/use.h duchain/forwarddeclaration.h duchain/duchainbase.h duchain/duchainpointer.h duchain/duchainlock.h duchain/identifier.h duchain/abstractfunctiondeclaration.h duchain/functiondeclaration.h duchain/stringhelpers.h duchain/safetycounter.h duchain/namespacealiasdeclaration.h duchain/aliasdeclaration.h duchain/dumpdotgraph.h duchain/duchainutils.h duchain/duchaindumper.h duchain/declarationid.h duchain/appendedlist.h duchain/duchainregister.h duchain/persistentsymboltable.h duchain/instantiationinformation.h duchain/specializationstore.h duchain/indexedducontext.h duchain/indexedtopducontext.h duchain/localindexedducontext.h duchain/indexeddeclaration.h duchain/localindexeddeclaration.h duchain/definitions.h duchain/problem.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/duchain COMPONENT Devel ) install(FILES duchain/types/unsuretype.h duchain/types/identifiedtype.h duchain/types/typesystem.h duchain/types/typeregister.h duchain/types/typerepository.h duchain/types/typepointer.h duchain/types/typesystemdata.h duchain/types/abstracttype.h duchain/types/integraltype.h duchain/types/functiontype.h duchain/types/structuretype.h duchain/types/pointertype.h duchain/types/referencetype.h duchain/types/delayedtype.h duchain/types/arraytype.h duchain/types/indexedtype.h duchain/types/enumerationtype.h duchain/types/constantintegraltype.h duchain/types/enumeratortype.h duchain/types/alltypes.h duchain/types/typeutils.h duchain/types/typealiastype.h duchain/types/containertypes.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/duchain/types COMPONENT Devel ) install(FILES duchain/builders/abstractcontextbuilder.h duchain/builders/abstractdeclarationbuilder.h duchain/builders/abstracttypebuilder.h duchain/builders/abstractusebuilder.h duchain/builders/dynamiclanguageexpressionvisitor.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/duchain/builders COMPONENT Devel ) install(FILES codecompletion/codecompletion.h codecompletion/codecompletionworker.h codecompletion/codecompletionmodel.h codecompletion/codecompletionitem.h codecompletion/codecompletioncontext.h codecompletion/codecompletionitemgrouper.h codecompletion/codecompletionhelper.h codecompletion/normaldeclarationcompletionitem.h codecompletion/abstractincludefilecompletionitem.h codecompletion/codecompletiontesthelper.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/codecompletion COMPONENT Devel ) install(FILES codegen/applychangeswidget.h codegen/astchangeset.h codegen/duchainchangeset.h codegen/documentchangeset.h codegen/coderepresentation.h codegen/utilities.h codegen/templatesmodel.h codegen/templatepreviewicon.h codegen/templaterenderer.h codegen/templateengine.h codegen/sourcefiletemplate.h codegen/templateclassgenerator.h codegen/codedescription.h codegen/basicrefactoring.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/codegen COMPONENT Devel ) install(FILES duchain/navigation/usesnavigationcontext.h duchain/navigation/abstractnavigationcontext.h duchain/navigation/abstractdeclarationnavigationcontext.h duchain/navigation/abstractincludenavigationcontext.h duchain/navigation/abstractnavigationwidget.h duchain/navigation/navigationaction.h duchain/navigation/useswidget.h duchain/navigation/usescollector.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/duchain/navigation COMPONENT Devel ) install(FILES highlighting/codehighlighting.h highlighting/colorcache.h highlighting/configurablecolors.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/highlighting COMPONENT Devel ) install(FILES checks/dataaccess.h checks/dataaccessrepository.h checks/controlflowgraph.h checks/controlflownode.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/checks COMPONENT Devel ) install(FILES classmodel/classmodel.h classmodel/classmodelnode.h classmodel/classmodelnodescontroller.h classmodel/allclassesfolder.h classmodel/documentclassesfolder.h classmodel/projectfolder.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kdevplatform/language/classmodel COMPONENT Devel ) diff --git a/kdevplatform/language/duchain/navigation/abstractnavigationcontext.cpp b/kdevplatform/language/duchain/navigation/abstractnavigationcontext.cpp index 1291ea515e..de5e319583 100644 --- a/kdevplatform/language/duchain/navigation/abstractnavigationcontext.cpp +++ b/kdevplatform/language/duchain/navigation/abstractnavigationcontext.cpp @@ -1,535 +1,569 @@ /* Copyright 2007 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "abstractnavigationcontext.h" #include #include #include "abstractdeclarationnavigationcontext.h" #include "abstractnavigationwidget.h" #include "usesnavigationcontext.h" #include "../../../interfaces/icore.h" #include "../../../interfaces/idocumentcontroller.h" #include "../functiondeclaration.h" #include "../namespacealiasdeclaration.h" #include "../types/functiontype.h" #include "../types/structuretype.h" #include #include #include #include namespace KDevelop { class AbstractNavigationContextPrivate { public: QVector m_children; //Used to keep alive all children until this is deleted int m_selectedLink = 0; //The link currently selected NavigationAction m_selectedLinkAction; //Target of the currently selected link bool m_shorten = false; //A counter used while building the html-code to count the used links. int m_linkCount = -1; //Something else than -1 if the current position is represented by a line-number, not a link. int m_currentLine = 0; int m_currentPositionLine = 0; QMap m_links; QMap m_linkLines; //Holds the line for each link QMap m_intLinks; AbstractNavigationContext* m_previousContext; TopDUContextPointer m_topContext; QString m_currentText; //Here the text is built }; void AbstractNavigationContext::setTopContext(const TopDUContextPointer& context) { d->m_topContext = context; } TopDUContextPointer AbstractNavigationContext::topContext() const { return d->m_topContext; } AbstractNavigationContext::AbstractNavigationContext(const TopDUContextPointer& topContext, AbstractNavigationContext* previousContext) : d(new AbstractNavigationContextPrivate) { d->m_previousContext = previousContext; d->m_topContext = topContext; qRegisterMetaType("KTextEditor::Cursor"); qRegisterMetaType("IDocumentation::Ptr"); } AbstractNavigationContext::~AbstractNavigationContext() { } void AbstractNavigationContext::makeLink(const QString& name, const DeclarationPointer& declaration, NavigationAction::Type actionType) { NavigationAction action(declaration, actionType); makeLink(name, QString(), action); } QString AbstractNavigationContext::createLink(const QString& name, const QString&, const NavigationAction& action) { if (d->m_shorten) { //Do not create links in shortened mode, it's only for viewing return typeHighlight(name.toHtmlEscaped()); } // NOTE: Since the by definition in the HTML standard some uri components // are case-insensitive, we define a new lowercase link-id for each // link. Otherwise Qt 5 seems to mess up the casing and the link // cannot be matched when it's executed. QString hrefId = QStringLiteral("link_%1").arg(d->m_links.count()); d->m_links[hrefId] = action; d->m_intLinks[d->m_linkCount] = action; d->m_linkLines[d->m_linkCount] = d->m_currentLine; if (d->m_currentPositionLine == d->m_currentLine) { d->m_currentPositionLine = -1; d->m_selectedLink = d->m_linkCount; } QString str = name.toHtmlEscaped(); if (d->m_linkCount == d->m_selectedLink) str = QLatin1String("") + str + QLatin1String( ""); QString ret = QLatin1String("m_linkCount == d->m_selectedLink && d->m_currentPositionLine == -1) ? QStringLiteral(" name = \"currentPosition\"") : QString()) + QLatin1Char('>') + str + QLatin1String(""); if (d->m_selectedLink == d->m_linkCount) d->m_selectedLinkAction = action; ++d->m_linkCount; return ret; } void AbstractNavigationContext::makeLink(const QString& name, const QString& targetId, const NavigationAction& action) { modifyHtml() += createLink(name, targetId, action); } void AbstractNavigationContext::clear() { d->m_linkCount = 0; d->m_currentLine = 0; d->m_currentText.clear(); d->m_links.clear(); d->m_intLinks.clear(); d->m_linkLines.clear(); } void AbstractNavigationContext::executeLink(const QString& link) { const auto actionIt = d->m_links.constFind(link); if (actionIt == d->m_links.constEnd()) return; execute(*actionIt); } NavigationContextPointer AbstractNavigationContext::executeKeyAction(const QString& key) { Q_UNUSED(key); return NavigationContextPointer(this); } NavigationContextPointer AbstractNavigationContext::execute(const NavigationAction& action) { if (action.targetContext) return NavigationContextPointer(action.targetContext); if (action.type == NavigationAction::ExecuteKey) return executeKeyAction(action.key); if (!action.decl && (action.type != NavigationAction::JumpToSource || action.document.isEmpty())) { qCDebug(LANGUAGE) << "Navigation-action has invalid declaration" << endl; return NavigationContextPointer(this); } switch (action.type) { case NavigationAction::ExecuteKey: break; case NavigationAction::None: qCDebug(LANGUAGE) << "Tried to execute an invalid action in navigation-widget" << endl; break; case NavigationAction::NavigateDeclaration: { auto ctx = dynamic_cast(d->m_previousContext); if (ctx && ctx->declaration() == action.decl) return NavigationContextPointer(d->m_previousContext); return registerChild(action.decl); } case NavigationAction::NavigateUses: { auto* browser = ICore::self()->pluginController()->extensionForPlugin(); if (browser) { browser->showUses(action.decl); return NavigationContextPointer(this); } Q_FALLTHROUGH(); } case NavigationAction::ShowUses: { return registerChild(new UsesNavigationContext(action.decl.data(), this)); } case NavigationAction::JumpToSource: { QUrl doc = action.document; KTextEditor::Cursor cursor = action.cursor; { DUChainReadLocker lock(DUChain::lock()); if (action.decl) { if (doc.isEmpty()) { doc = action.decl->url().toUrl(); /* if(action.decl->internalContext()) cursor = action.decl->internalContext()->range().start() + KTextEditor::Cursor(0, 1); else*/ cursor = action.decl->rangeInCurrentRevision().start(); } action.decl->activateSpecialization(); } } //This is used to execute the slot delayed in the event-loop, so crashes are avoided QMetaObject::invokeMethod(ICore::self()->documentController(), "openDocument", Qt::QueuedConnection, Q_ARG(QUrl, doc), Q_ARG(KTextEditor::Cursor, cursor)); break; } case NavigationAction::ShowDocumentation: { auto doc = ICore::self()->documentationController()->documentationForDeclaration(action.decl.data()); // This is used to execute the slot delayed in the event-loop, so crashes are avoided // which can happen e.g. due to focus change events resulting in tooltip destruction and thus this object QMetaObject::invokeMethod( ICore::self()->documentationController(), "showDocumentation", Qt::QueuedConnection, Q_ARG(IDocumentation::Ptr, doc)); } break; } return NavigationContextPointer(this); } AbstractNavigationContext* AbstractNavigationContext::previousContext() const { return d->m_previousContext; } void AbstractNavigationContext::setPreviousContext(AbstractNavigationContext* previous) { d->m_previousContext = previous; } NavigationContextPointer AbstractNavigationContext::registerChild(AbstractNavigationContext* context) { d->m_children << NavigationContextPointer(context); return d->m_children.last(); } NavigationContextPointer AbstractNavigationContext::registerChild(const DeclarationPointer& declaration) { //We create a navigation-widget here, and steal its context.. evil ;) QScopedPointer navigationWidget( declaration->context()->createNavigationWidget(declaration.data())); if (navigationWidget) { NavigationContextPointer ret = navigationWidget->context(); ret->setPreviousContext(this); d->m_children << ret; return ret; } else { return NavigationContextPointer(this); } } const int lineJump = 3; -void AbstractNavigationContext::down() +bool AbstractNavigationContext::down() { //Make sure link-count is valid if (d->m_linkCount == -1) { DUChainReadLocker lock; html(); } + // select first link when we enter via down + if (d->m_selectedLink == -1 && d->m_linkCount) { + d->m_selectedLink = 0; + d->m_currentPositionLine = -1; + return true; + } + int fromLine = d->m_currentPositionLine; // try to select the next link within our lineJump distance if (d->m_selectedLink >= 0 && d->m_selectedLink < d->m_linkCount) { if (fromLine == -1) fromLine = d->m_linkLines[d->m_selectedLink]; for (int newSelectedLink = d->m_selectedLink + 1; newSelectedLink < d->m_linkCount; ++newSelectedLink) { if (d->m_linkLines[newSelectedLink] > fromLine && d->m_linkLines[newSelectedLink] - fromLine <= lineJump) { d->m_selectedLink = newSelectedLink; d->m_currentPositionLine = -1; - return; + return true; } } } if (fromLine == d->m_currentLine - 1) // nothing to do, we are at the end of the document - return; + return false; // scroll down by applying the lineJump if (fromLine == -1) fromLine = 0; d->m_currentPositionLine = fromLine + lineJump; - if (d->m_currentPositionLine >= d->m_currentLine) + if (d->m_currentPositionLine >= d->m_currentLine) { d->m_currentPositionLine = d->m_currentLine - 1; + } + return fromLine != d->m_currentPositionLine; } -void AbstractNavigationContext::up() +bool AbstractNavigationContext::up() { //Make sure link-count is valid if (d->m_linkCount == -1) { DUChainReadLocker lock; html(); } + // select last link when we enter via up + if (d->m_selectedLink == -1 && d->m_linkCount) { + d->m_selectedLink = d->m_linkCount - 1; + d->m_currentPositionLine = -1; + return true; + } + int fromLine = d->m_currentPositionLine; if (d->m_selectedLink >= 0 && d->m_selectedLink < d->m_linkCount) { if (fromLine == -1) fromLine = d->m_linkLines[d->m_selectedLink]; for (int newSelectedLink = d->m_selectedLink - 1; newSelectedLink >= 0; --newSelectedLink) { if (d->m_linkLines[newSelectedLink] < fromLine && fromLine - d->m_linkLines[newSelectedLink] <= lineJump) { d->m_selectedLink = newSelectedLink; d->m_currentPositionLine = -1; - return; + return true; } } } if (fromLine == -1) fromLine = d->m_currentLine - 1; d->m_currentPositionLine = fromLine - lineJump; if (d->m_currentPositionLine < 0) d->m_currentPositionLine = 0; + + return fromLine || d->m_currentPositionLine; } -void AbstractNavigationContext::nextLink() +bool AbstractNavigationContext::nextLink() { //Make sure link-count is valid if (d->m_linkCount == -1) { DUChainReadLocker lock; html(); } + if (!d->m_linkCount) + return false; + d->m_currentPositionLine = -1; - if (d->m_linkCount > 0) - d->m_selectedLink = (d->m_selectedLink + 1) % d->m_linkCount; + d->m_selectedLink++; + if (d->m_selectedLink >= d->m_linkCount) { + d->m_selectedLink = 0; + return false; + } + return true; } -void AbstractNavigationContext::previousLink() +bool AbstractNavigationContext::previousLink() { //Make sure link-count is valid if (d->m_linkCount == -1) { DUChainReadLocker lock; html(); } + if (!d->m_linkCount) + return false; + d->m_currentPositionLine = -1; - if (d->m_linkCount > 0) { - --d->m_selectedLink; - if (d->m_selectedLink < 0) - d->m_selectedLink += d->m_linkCount; + d->m_selectedLink--; + if (d->m_selectedLink < 0) { + d->m_selectedLink = d->m_linkCount - 1; + return false; } - - Q_ASSERT(d->m_selectedLink >= 0); + return true; } int AbstractNavigationContext::linkCount() const { return d->m_linkCount; } +void AbstractNavigationContext::resetNavigation() +{ + d->m_currentPositionLine = -1; + d->m_selectedLink = -1; + d->m_selectedLinkAction = {}; +} + NavigationContextPointer AbstractNavigationContext::back() { if (d->m_previousContext) return NavigationContextPointer(d->m_previousContext); else return NavigationContextPointer(this); } NavigationContextPointer AbstractNavigationContext::accept() { if (d->m_selectedLink >= 0 && d->m_selectedLink < d->m_linkCount) { NavigationAction action = d->m_intLinks[d->m_selectedLink]; return execute(action); } return NavigationContextPointer(this); } NavigationContextPointer AbstractNavigationContext::accept(IndexedDeclaration decl) { if (decl.data()) { NavigationAction action(DeclarationPointer(decl.data()), NavigationAction::NavigateDeclaration); return execute(action); } else { return NavigationContextPointer(this); } } NavigationContextPointer AbstractNavigationContext::acceptLink(const QString& link) { const auto actionIt = d->m_links.constFind(link); if (actionIt == d->m_links.constEnd()) { qCDebug(LANGUAGE) << "Executed unregistered link " << link << endl; return NavigationContextPointer(this); } return execute(*actionIt); } NavigationAction AbstractNavigationContext::currentAction() const { return d->m_selectedLinkAction; } QString AbstractNavigationContext::declarationKind(const DeclarationPointer& decl) { const auto* function = dynamic_cast(decl.data()); QString kind; if (decl->isTypeAlias()) kind = i18n("Typedef"); else if (decl->kind() == Declaration::Type) { if (decl->type()) kind = i18n("Class"); } else if (decl->kind() == Declaration::Instance) { kind = i18n("Variable"); } else if (decl->kind() == Declaration::Namespace) { kind = i18n("Namespace"); } if (auto* alias = dynamic_cast(decl.data())) { if (alias->identifier().isEmpty()) kind = i18n("Namespace import"); else kind = i18n("Namespace alias"); } if (function) kind = i18n("Function"); if (decl->isForwardDeclaration()) kind = i18n("Forward Declaration"); return kind; } QString AbstractNavigationContext::html(bool shorten) { d->m_shorten = shorten; return QString(); } bool AbstractNavigationContext::alreadyComputed() const { return !d->m_currentText.isEmpty(); } bool AbstractNavigationContext::isWidgetMaximized() const { return true; } QWidget* AbstractNavigationContext::widget() const { return nullptr; } ///Splits the string by the given regular expression, but keeps the split-matches at the end of each line static QStringList splitAndKeep(QString str, const QRegExp& regExp) { QStringList ret; int place = regExp.indexIn(str); while (place != -1) { ret << str.left(place + regExp.matchedLength()); str.remove(0, place + regExp.matchedLength()); place = regExp.indexIn(str); } ret << str; return ret; } void AbstractNavigationContext::addHtml(const QString& html) { QRegExp newLineRegExp(QStringLiteral("
|
|

")); foreach (const QString& line, splitAndKeep(html, newLineRegExp)) { d->m_currentText += line; if (line.indexOf(newLineRegExp) != -1) { ++d->m_currentLine; if (d->m_currentLine == d->m_currentPositionLine) { d->m_currentText += QStringLiteral( " <-> "); // ><-> is <-> } } } } QString AbstractNavigationContext::currentHtml() const { return d->m_currentText; } QString Colorizer::operator()(const QString& str) const { QString ret = QLatin1String("") + str + QLatin1String(""); if (m_formatting & Fixed) ret = QLatin1String("") + ret + QLatin1String(""); if (m_formatting & Bold) ret = QLatin1String("") + ret + QLatin1String(""); if (m_formatting & Italic) ret = QLatin1String("") + ret + QLatin1String(""); return ret; } const Colorizer AbstractNavigationContext::typeHighlight(QStringLiteral("006000")); const Colorizer AbstractNavigationContext::errorHighlight(QStringLiteral("990000")); const Colorizer AbstractNavigationContext::labelHighlight(QStringLiteral("000000")); const Colorizer AbstractNavigationContext::codeHighlight(QStringLiteral("005000")); const Colorizer AbstractNavigationContext::propertyHighlight(QStringLiteral("009900")); const Colorizer AbstractNavigationContext::navigationHighlight(QStringLiteral("000099")); const Colorizer AbstractNavigationContext::importantHighlight(QStringLiteral( "000000"), Colorizer::Bold | Colorizer::Italic); const Colorizer AbstractNavigationContext::commentHighlight(QStringLiteral("303030")); const Colorizer AbstractNavigationContext::nameHighlight(QStringLiteral("000000"), Colorizer::Bold); } diff --git a/kdevplatform/language/duchain/navigation/abstractnavigationcontext.h b/kdevplatform/language/duchain/navigation/abstractnavigationcontext.h index 3eb033e285..314b419db2 100644 --- a/kdevplatform/language/duchain/navigation/abstractnavigationcontext.h +++ b/kdevplatform/language/duchain/navigation/abstractnavigationcontext.h @@ -1,174 +1,173 @@ /* Copyright 2007 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_ABSTRACTNAVIGATIONCONTEXT_H #define KDEVPLATFORM_ABSTRACTNAVIGATIONCONTEXT_H #include #include #include "../indexeddeclaration.h" #include "navigationaction.h" namespace KDevelop { /** A helper-class for elegant colorization of html-strings . * * Initialize it with a html-color like "990000". and colorize strings * using operator() */ struct KDEVPLATFORMLANGUAGE_EXPORT Colorizer { enum FormattingFlag { Nothing = 0x0, Bold = 0x1, Italic = 0x2, Fixed = 0x4 }; Q_DECLARE_FLAGS(Formatting, FormattingFlag) explicit Colorizer(const QString& color, Formatting formatting = Nothing) : m_color(color) , m_formatting(formatting) { } QString operator()(const QString& str) const; QString m_color; Formatting m_formatting; }; class AbstractNavigationContext; using NavigationContextPointer = QExplicitlySharedDataPointer; class KDEVPLATFORMLANGUAGE_EXPORT AbstractNavigationContext : public QObject , public QSharedData { Q_OBJECT public: explicit AbstractNavigationContext(const TopDUContextPointer& topContext = TopDUContextPointer(), AbstractNavigationContext* previousContext = nullptr); ~AbstractNavigationContext() override; - void nextLink(); - void previousLink(); - int linkCount() const; - - void up(); - void down(); + bool nextLink(); + bool previousLink(); + bool up(); + bool down(); + void resetNavigation(); NavigationContextPointer accept(); NavigationContextPointer back(); NavigationContextPointer accept(IndexedDeclaration decl); NavigationContextPointer acceptLink(const QString& link); NavigationAction currentAction() const; virtual QString name() const = 0; ///Here the context can return html to be displayed. ///NOTE: The DUChain must be locked while this is called. virtual QString html(bool shorten = false); ///Here the context can return a widget to be displayed. ///The widget stays owned by this navigation-context. ///The widget may have a signal "navigateDeclaration(KDevelop::IndexedDeclaration)". ///If that signal is emitted, the new declaration is navigated in the navigation-wdiget. virtual QWidget* widget() const; ///Whether the widget returned by widget() should take the maximum possible spsace. ///The default implementation returns true. virtual bool isWidgetMaximized() const; ///Returns whether this context's string has already been computed, and is up to date. ///After clear() was called, this returns false again. bool alreadyComputed() const; TopDUContextPointer topContext() const; void setTopContext(const TopDUContextPointer& context); void executeLink(const QString& link); NavigationContextPointer execute(const NavigationAction& action); Q_SIGNALS: void contentsChanged(); protected: AbstractNavigationContext* previousContext() const; virtual void setPreviousContext(AbstractNavigationContext* previousContext); struct TextHandler { explicit TextHandler(AbstractNavigationContext* c) : context(c) { } void operator+=(const QString& str) const { context->addHtml(str); } AbstractNavigationContext* context; }; ///Override this to execute own key-actions using NavigationAction virtual NavigationContextPointer executeKeyAction(const QString& key); ///Adds given the text to currentHtml() void addHtml(const QString& html); ///Returns the html text being built in its current state QString currentHtml() const; ///Returns a convenience object that allows writing "modifyHtml() += "Hallo";" TextHandler modifyHtml() { return TextHandler(this); } //Clears the computed html and links void clear(); ///Creates and registers a link to the given declaration, labeled by the given name virtual void makeLink(const QString& name, const DeclarationPointer& declaration, NavigationAction::Type actionType); ///Creates a link that executes the given action and adds it to the current context void makeLink(const QString& name, const QString& targetId, const NavigationAction& action); ///Creates a link that executes the given action and returns it QString createLink(const QString& name, const QString& targetId, const NavigationAction& action); NavigationContextPointer registerChild(const DeclarationPointer& /*declaration*/); NavigationContextPointer registerChild(AbstractNavigationContext* context); virtual QString declarationKind(const DeclarationPointer& decl); static const Colorizer typeHighlight; static const Colorizer errorHighlight; static const Colorizer labelHighlight; static const Colorizer codeHighlight; static const Colorizer propertyHighlight; static const Colorizer navigationHighlight; static const Colorizer importantHighlight; static const Colorizer commentHighlight; static const Colorizer nameHighlight; private: const QScopedPointer d; }; } Q_DECLARE_OPERATORS_FOR_FLAGS(KDevelop::Colorizer::Formatting) #endif diff --git a/kdevplatform/language/duchain/navigation/abstractnavigationwidget.cpp b/kdevplatform/language/duchain/navigation/abstractnavigationwidget.cpp index 68acda7cad..34a5d25634 100644 --- a/kdevplatform/language/duchain/navigation/abstractnavigationwidget.cpp +++ b/kdevplatform/language/duchain/navigation/abstractnavigationwidget.cpp @@ -1,346 +1,356 @@ /* Copyright 2007 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "abstractnavigationwidget.h" #include #include #include #include #include #include #include #include "../duchainlock.h" #include namespace { const int maxNavigationWidgetWidth = 580; const int maxNavigationWidgetHeight = 400; } namespace KDevelop { class AbstractNavigationWidgetPrivate { public: AbstractNavigationWidgetPrivate(AbstractNavigationWidget* q) : q(q) {} void anchorClicked(const QUrl&); AbstractNavigationWidget* q; NavigationContextPointer m_startContext; QPointer m_browser; QWidget* m_currentWidget = nullptr; QString m_currentText; mutable QSize m_idealTextSize; AbstractNavigationWidget::DisplayHints m_hints = AbstractNavigationWidget::NoHints; NavigationContextPointer m_context; }; AbstractNavigationWidget::AbstractNavigationWidget() : d(new AbstractNavigationWidgetPrivate(this)) { setPalette(QApplication::palette()); setFocusPolicy(Qt::NoFocus); resize(100, 100); } QSize AbstractNavigationWidget::sizeHint() const { if (d->m_browser) { updateIdealSize(); QSize ret = QSize(qMin(d->m_idealTextSize.width(), maxNavigationWidgetWidth), qMin(d->m_idealTextSize.height(), maxNavigationWidgetHeight)); if (d->m_idealTextSize.height() >= maxNavigationWidgetHeight) { //make space for the scrollbar in case it's not fitting ret.rwidth() += 17; //m_browser->verticalScrollBar()->width() returns 100, for some reason } if (d->m_currentWidget) { ret.setHeight(ret.height() + d->m_currentWidget->sizeHint().height()); if (d->m_currentWidget->sizeHint().width() > ret.width()) ret.setWidth(d->m_currentWidget->sizeHint().width()); if (ret.width() < 500) //When we embed a widget, give it some space, even if it doesn't have a large size-hint ret.setWidth(500); } return ret; } else return QWidget::sizeHint(); } void AbstractNavigationWidget::initBrowser(int height) { Q_ASSERT(!d->m_browser); Q_UNUSED(height); d->m_browser = new QTextBrowser; // since we can embed arbitrary HTML we have to make sure it stays readable by forcing a black-white palette QPalette p; p.setColor(QPalette::AlternateBase, Qt::white); p.setColor(QPalette::Base, Qt::white); p.setColor(QPalette::Text, Qt::black); d->m_browser->setPalette(p); d->m_browser->setOpenLinks(false); d->m_browser->setOpenExternalLinks(false); auto* layout = new QVBoxLayout; layout->addWidget(d->m_browser); layout->setMargin(0); setLayout(layout); connect(d->m_browser.data(), &QTextEdit::selectionChanged, this, [this]() { d->m_browser->copy(); }); connect(d->m_browser.data(), &QTextBrowser::anchorClicked, this, [&](const QUrl& url) { d->anchorClicked(url); }); foreach (QWidget* w, findChildren()) w->setContextMenuPolicy(Qt::NoContextMenu); } AbstractNavigationWidget::~AbstractNavigationWidget() { if (d->m_currentWidget) layout()->removeWidget(d->m_currentWidget); } void AbstractNavigationWidget::setContext(NavigationContextPointer context, int initBrows) { if (d->m_browser == nullptr) initBrowser(initBrows); if (!context) { qCDebug(LANGUAGE) << "no new context created"; return; } if (context == d->m_context && (!context || context->alreadyComputed())) return; if (!d->m_startContext) { d->m_startContext = context; } bool wasInitial = (d->m_context == d->m_startContext); d->m_context = context; update(); emit contextChanged(wasInitial, d->m_context == d->m_startContext); emit sizeHintChanged(); } void AbstractNavigationWidget::updateIdealSize() const { if (d->m_context && !d->m_idealTextSize.isValid()) { QTextDocument doc; doc.setHtml(d->m_currentText); if (doc.idealWidth() > maxNavigationWidgetWidth) { doc.setTextWidth(maxNavigationWidgetWidth); d->m_idealTextSize.setWidth(maxNavigationWidgetWidth); } else { d->m_idealTextSize.setWidth(doc.idealWidth()); } d->m_idealTextSize.setHeight(doc.size().height()); } } void AbstractNavigationWidget::setDisplayHints(DisplayHints hints) { d->m_hints = hints; } void AbstractNavigationWidget::update() { setUpdatesEnabled(false); Q_ASSERT(d->m_context); QString html; { DUChainReadLocker lock; html = d->m_context->html(); } if (!html.isEmpty()) { int scrollPos = d->m_browser->verticalScrollBar()->value(); if (!(d->m_hints & EmbeddableWidget)) { // TODO: Only show that the first time, or the first few times this context is shown? html += QStringLiteral("

"); if (d->m_context->linkCount() > 0) { html += i18n("(Hold Alt to show. Navigate via arrow keys, activate by pressing Enter)"); } else { html += i18n("(Hold Alt to show this tooltip)"); } html += QStringLiteral("

"); } d->m_browser->setHtml(html); d->m_currentText = html; d->m_idealTextSize = QSize(); QSize hint = sizeHint(); if (hint.height() >= d->m_idealTextSize.height()) { d->m_browser->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } else { d->m_browser->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); } d->m_browser->verticalScrollBar()->setValue(scrollPos); d->m_browser->scrollToAnchor(QStringLiteral("currentPosition")); d->m_browser->show(); } else { d->m_browser->hide(); } if (d->m_currentWidget) { layout()->removeWidget(d->m_currentWidget); d->m_currentWidget->setParent(nullptr); } d->m_currentWidget = d->m_context->widget(); d->m_browser->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); d->m_browser->setMaximumHeight(10000); if (d->m_currentWidget) { const auto signalSignature = QMetaObject::normalizedSignature( "navigateDeclaration(KDevelop::IndexedDeclaration)"); if (d->m_currentWidget->metaObject()->indexOfSignal(signalSignature.constData()) != -1) { connect(d->m_currentWidget, SIGNAL(navigateDeclaration(KDevelop::IndexedDeclaration)), this, SLOT(navigateDeclaration(KDevelop::IndexedDeclaration))); } layout()->addWidget(d->m_currentWidget); if (d->m_context->isWidgetMaximized()) { //Leave unused room to the widget d->m_browser->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); d->m_browser->setMaximumHeight(d->m_idealTextSize.height()); } } setUpdatesEnabled(true); } NavigationContextPointer AbstractNavigationWidget::context() { return d->m_context; } void AbstractNavigationWidget::navigateDeclaration(const IndexedDeclaration& decl) { setContext(d->m_context->accept(decl)); } void AbstractNavigationWidgetPrivate::anchorClicked(const QUrl& url) { DUChainReadLocker lock; //We may get deleted while the call to acceptLink, so make sure we don't crash in that case QPointer thisPtr(q); NavigationContextPointer nextContext = m_context->acceptLink(url.toString()); if (thisPtr) q->setContext(nextContext); } -void AbstractNavigationWidget::next() +bool AbstractNavigationWidget::next() { Q_ASSERT(d->m_context); - d->m_context->nextLink(); + auto ret = d->m_context->nextLink(); update(); + return ret; } -void AbstractNavigationWidget::previous() +bool AbstractNavigationWidget::previous() { Q_ASSERT(d->m_context); - d->m_context->previousLink(); + auto ret = d->m_context->previousLink(); update(); + return ret; } void AbstractNavigationWidget::accept() { Q_ASSERT(d->m_context); QPointer thisPtr(this); NavigationContextPointer nextContext = d->m_context->accept(); if (thisPtr) setContext(nextContext); } void AbstractNavigationWidget::back() { QPointer thisPtr(this); NavigationContextPointer nextContext = d->m_context->back(); if (thisPtr) setContext(nextContext); } -void AbstractNavigationWidget::up() +bool AbstractNavigationWidget::up() { - d->m_context->up(); + auto ret = d->m_context->up(); update(); + return ret; } -void AbstractNavigationWidget::down() +bool AbstractNavigationWidget::down() { - d->m_context->down(); + auto ret = d->m_context->down(); + update(); + return ret; +} + +void AbstractNavigationWidget::resetNavigationState() +{ + d->m_context->resetNavigation(); update(); } void AbstractNavigationWidget::embeddedWidgetAccept() { accept(); } void AbstractNavigationWidget::embeddedWidgetDown() { down(); } void AbstractNavigationWidget::embeddedWidgetRight() { next(); } void AbstractNavigationWidget::embeddedWidgetLeft() { previous(); } void AbstractNavigationWidget::embeddedWidgetUp() { up(); } void AbstractNavigationWidget::wheelEvent(QWheelEvent* event) { QWidget::wheelEvent(event); event->accept(); } } #include "moc_abstractnavigationwidget.cpp" diff --git a/kdevplatform/language/duchain/navigation/abstractnavigationwidget.h b/kdevplatform/language/duchain/navigation/abstractnavigationwidget.h index 898c3a0a1c..c910443e32 100644 --- a/kdevplatform/language/duchain/navigation/abstractnavigationwidget.h +++ b/kdevplatform/language/duchain/navigation/abstractnavigationwidget.h @@ -1,98 +1,99 @@ /* Copyright 2007 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_ABSTRACTNAVIGATIONWIDGET_H #define KDEVPLATFORM_ABSTRACTNAVIGATIONWIDGET_H #include #include #include #include "../../interfaces/quickopendataprovider.h" #include "abstractnavigationcontext.h" class QTextBrowser; namespace KDevelop { /** * This class deleted itself when its part is deleted, so always use a QPointer when referencing it. * The duchain must be read-locked for most operations * */ class KDEVPLATFORMLANGUAGE_EXPORT AbstractNavigationWidget : public QWidget , public QuickOpenEmbeddedWidgetInterface { Q_OBJECT public: enum DisplayHint { NoHints = 0x0, // < Normal display EmbeddableWidget = 0x1, // < Omit parts which are only useful for the navigation popup }; Q_DECLARE_FLAGS(DisplayHints, DisplayHint) AbstractNavigationWidget(); ~AbstractNavigationWidget() override; void setContext(NavigationContextPointer context, int initBrowser = 400); void setDisplayHints(DisplayHints hints); QSize sizeHint() const override; public Q_SLOTS: /// keyboard navigation support - void next() override; - void previous() override; + bool next() override; + bool previous() override; + bool up() override; + bool down() override; void accept() override; - void up() override; - void down() override; void back() override; + void resetNavigationState() override; ///These are temporarily for gettings these events directly from kate ///@todo Do this through a public interface post 4.2 void embeddedWidgetRight(); ///Keyboard-action "previous" void embeddedWidgetLeft(); ///Keyboard-action "accept" void embeddedWidgetAccept(); void embeddedWidgetUp(); void embeddedWidgetDown(); NavigationContextPointer context(); void navigateDeclaration(const KDevelop::IndexedDeclaration& decl); Q_SIGNALS: void sizeHintChanged(); /// Emitted whenever the current navigation-context has changed /// @param wasInitial whether the previous context was the initial context /// @param isInitial whether the current context is the initial context void contextChanged(bool wasInitial, bool isInitial); protected: void wheelEvent(QWheelEvent*) override; void updateIdealSize() const; void initBrowser(int height); void update(); private: const QScopedPointer d; }; } #endif diff --git a/kdevplatform/language/duchain/navigation/quickopenembeddedwidgetcombiner.cpp b/kdevplatform/language/duchain/navigation/quickopenembeddedwidgetcombiner.cpp new file mode 100644 index 0000000000..38be3d1028 --- /dev/null +++ b/kdevplatform/language/duchain/navigation/quickopenembeddedwidgetcombiner.cpp @@ -0,0 +1,170 @@ +/* + * This file is part of KDevelop + * + * Copyright 2019 Milian Wolff + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "quickopenembeddedwidgetcombiner.h" + +#include + +using namespace KDevelop; + +QuickOpenEmbeddedWidgetInterface* toInterface(QObject *object) +{ + return dynamic_cast(object); +} + +struct QuickOpenEmbeddedWidgetCombiner::Private +{ + QuickOpenEmbeddedWidgetInterface* currentChild = nullptr; + + bool init(const QObjectList& children) + { + if (currentChild) + return true; + + currentChild = nextChild(Next, children); + return currentChild; + } + + enum Action + { + Next, + Previous, + Up, + Down, + }; + QuickOpenEmbeddedWidgetInterface* nextChild(Action action, const QObjectList& children) const + { + if (action == Next || action == Down) { + return nextChild(children.begin(), children.end()); + } else { + return nextChild(children.rbegin(), children.rend()); + } + } + + template + QuickOpenEmbeddedWidgetInterface* nextChild(const It start, const It end) const + { + if (start == end) + return nullptr; + + auto current = start; + if (currentChild) { + current = std::find_if(start, end, [this](QObject *child) { + return toInterface(child) == currentChild; + }); + } + + auto it = std::find_if(current + 1, end, toInterface); + if (it == end && current != start) { + // cycle + it = std::find_if(start, current, toInterface); + } + return (it != end) ? toInterface(*it) : nullptr; + } + + bool navigate(Action action, const QObjectList& children) + { + if (!init(children)) { + return false; + } + + auto applyAction = [action](QuickOpenEmbeddedWidgetInterface* interface) + { + switch (action) { + case Next: + return interface->next(); + case Previous: + return interface->previous(); + case Up: + return interface->up(); + case Down: + return interface->down(); + } + Q_UNREACHABLE(); + }; + + if (applyAction(currentChild)) { + return true; + } + + if (auto *next = nextChild(action, children)) { + currentChild->resetNavigationState(); + currentChild = next; + auto ret = applyAction(currentChild); + return ret; + } + return false; + } +}; + +QuickOpenEmbeddedWidgetCombiner::QuickOpenEmbeddedWidgetCombiner(QWidget* parent) + : QWidget(parent) + , d(new Private) +{ + setLayout(new QVBoxLayout); + layout()->setContentsMargins(2, 2, 2, 2); + layout()->setSpacing(0); +} + +QuickOpenEmbeddedWidgetCombiner::~QuickOpenEmbeddedWidgetCombiner() = default; + +bool QuickOpenEmbeddedWidgetCombiner::next() +{ + return d->navigate(Private::Next, children()); +} + +bool QuickOpenEmbeddedWidgetCombiner::previous() +{ + return d->navigate(Private::Previous, children()); +} + +bool QuickOpenEmbeddedWidgetCombiner::up() +{ + return d->navigate(Private::Up, children()); +} + +bool QuickOpenEmbeddedWidgetCombiner::down() +{ + return d->navigate(Private::Down, children()); +} + +void QuickOpenEmbeddedWidgetCombiner::back() +{ + if (d->currentChild) { + d->currentChild->back(); + } +} + +void QuickOpenEmbeddedWidgetCombiner::accept() +{ + if (d->currentChild) { + d->currentChild->accept(); + } +} + +void QuickOpenEmbeddedWidgetCombiner::resetNavigationState() +{ + for (auto* child : children()) { + if (auto* interface = toInterface(child)) { + interface->resetNavigationState(); + } + } +} diff --git a/kdevplatform/language/duchain/navigation/quickopenembeddedwidgetcombiner.h b/kdevplatform/language/duchain/navigation/quickopenembeddedwidgetcombiner.h new file mode 100644 index 0000000000..e11d856514 --- /dev/null +++ b/kdevplatform/language/duchain/navigation/quickopenembeddedwidgetcombiner.h @@ -0,0 +1,55 @@ +/* + * This file is part of KDevelop + * + * Copyright 2019 Milian Wolff + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#ifndef KDEVPLATFORM_QUICKOPENEMBEDDEDWIDGETCOMBINER_H +#define KDEVPLATFORM_QUICKOPENEMBEDDEDWIDGETCOMBINER_H + +#include + +#include + +namespace KDevelop { +/** + * A widget that implements the QuickOpenEmbeddedWidgetInterface by asking its direct children. + * + * I.e. add widgets into the combiner's layout. If the widgets support the QuickOpenEmbeddedWidgetInterface, + * then they will be used to implement the keyboard navigation features. + */ +class KDEVPLATFORMLANGUAGE_EXPORT QuickOpenEmbeddedWidgetCombiner : public QWidget, public QuickOpenEmbeddedWidgetInterface +{ +public: + explicit QuickOpenEmbeddedWidgetCombiner(QWidget* parent = nullptr); + ~QuickOpenEmbeddedWidgetCombiner() override; + + bool next() override; + bool previous() override; + bool up() override; + bool down() override; + void back() override; + void accept() override; + void resetNavigationState() override; + +private: + struct Private; + QScopedPointer d; +}; +} + +#endif // KDEVPLATFORM_QUICKOPENEMBEDDEDWIDGETCOMBINER_H diff --git a/kdevplatform/language/interfaces/quickopendataprovider.h b/kdevplatform/language/interfaces/quickopendataprovider.h index 4b39de46d4..5c0c8195ae 100644 --- a/kdevplatform/language/interfaces/quickopendataprovider.h +++ b/kdevplatform/language/interfaces/quickopendataprovider.h @@ -1,213 +1,215 @@ /* * This file is part of KDevelop * * Copyright 2007 David Nolden * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_QUICKOPENDATAPROVIDER_H #define KDEVPLATFORM_QUICKOPENDATAPROVIDER_H #include #include #include #include #include class QString; class QStringList; class QIcon; namespace KDevelop { class IndexedString; /** * Hint: When implementing a data-provider, do not forget to export it! Else it won't work. * */ /** * If your plugin manages a list of files, you can use this to return that list. * The file-list can be queried by other data-providers(for example functions/methods) so they * can manipulate their content based on those file-lists. The file-list should not be filtered at all, * it should only depend on the enabled models/items * * Example: A list of files in the include-path, a list of files in the project, etc. * */ class KDEVPLATFORMLANGUAGE_EXPORT QuickOpenFileSetInterface { public: virtual QSet files() const = 0; virtual ~QuickOpenFileSetInterface(); }; /** * You can use this as additional base-class for your embedded widgets to get additional interaction * */ class KDEVPLATFORMLANGUAGE_EXPORT QuickOpenEmbeddedWidgetInterface { public: virtual ~QuickOpenEmbeddedWidgetInterface(); ///Is called when the keyboard-shortcut "next" is triggered on the widget, which currently is ALT+Right - virtual void next() = 0; + virtual bool next() = 0; ///Is called when the keyboard-shortcut "previous" is triggered on the widget, which currently is ALT+Left - virtual void previous() = 0; - ///Is called when the keyboard-shortcut "accept" is triggered on the widget, which currently is ALT+Return - virtual void accept() = 0; + virtual bool previous() = 0; ///Is called when the keyboard-shortcut "scroll up" is triggered on the widget, which currently is ALT+Up - virtual void up() = 0; + virtual bool up() = 0; ///Is called when the keyboard-shortcut "scroll down" is triggered on the widget, which currently is ALT+Down - virtual void down() = 0; + virtual bool down() = 0; ///Is called when the keyboard-shortcut "back" is triggered on the widget, which currently is ALT+Backspace virtual void back() = 0; + ///Is called when the keyboard-shortcut "accept" is triggered on the widget, which currently is ALT+Return + virtual void accept() = 0; + ///Reset the navigation state to the state before keyboard interaction + virtual void resetNavigationState() = 0; }; /** * Reimplement this to represent single entries within the quickopen list. * */ class KDEVPLATFORMLANGUAGE_EXPORT QuickOpenDataBase : public QSharedData { public: virtual ~QuickOpenDataBase(); ///Return the text to be shown in the list for this item virtual QString text() const = 0; virtual QString htmlDescription() const = 0; /**Can return Custom highlighting triplets as explained in * the kde header ktexteditor/codecompletionmodel.h * The default-implementation returns an empty list, which means no * special highlighting will be applied. * */ virtual QList highlighting() const; /** * May return an icon to mark the item in the quickopen-list. * The standard-implementation returns an invalid item, which means that * no icon will be shown. * */ virtual QIcon icon() const; /** * Is called when the item should be executed. * * @param filterText Current content of the quickopen-dialogs filter line-edit. * If this is changed, and false is returned, the content of the * line-edit will be changed according to the new text. * @return Whether the dialog should be closed. * */ virtual bool execute(QString& filterText) = 0; /** * Return true here if this data-item should be expandable with * an own embedded widget. * The default-implementation returns false. * */ virtual bool isExpandable() const; /** * This will be called if isExpandable() returns true. * * A widget should be returned that will be embedded into the quickopen-list. * The widget will be owned by the quickopen-list and will be deleted at will. * * If the widget can be dynamic_cast'ed to QuickOpenEmbeddedWidgetInterface, * the additional interaction defined there will be possible. * * The default-implementation returns 0, which means no widget will be shown. * */ virtual QWidget* expandingWidget() const; }; using QuickOpenDataPointer = QExplicitlySharedDataPointer; /** * Use this interface to provide custom quickopen-data to the quickopen-widget. * * If possible, you should use KDevelop::Filter (@file quickopenfilter.h ) * to implement the actual filtering, so it is consistent. * */ class KDEVPLATFORMLANGUAGE_EXPORT QuickOpenDataProviderBase : public QObject { Q_OBJECT public: ~QuickOpenDataProviderBase() override; /** * For efficiency, all changes to the filter-text are provided by the following 3 difference-operations. * */ /** * Search-text was changed. * This is called whenever the search-text was changed, and the UI should be updated. * Store the text to track the exact difference. * */ virtual void setFilterText(const QString& text) = 0; /** * Filter-text should be completely reset and the context re-computed. * */ virtual void reset() = 0; /** * Returns the count of items this provider currently represents * */ virtual uint itemCount() const = 0; /** * Returns the count of *unfiltered* items this provider currently represents */ virtual uint unfilteredItemCount() const = 0; /** * Returns the data-item for a given row. * * Generally, the items must addressed alphabetically, * they will be displayed in the same order in the * quickopen list. * * For performance-reasons the underlying models should * create the QuickOpenDataBase items on demand, because only * those that will really be shown will be requested. * * @param row Index of item to be returned. * */ virtual QuickOpenDataPointer data(uint row) const = 0; /** * If the data-provider supports multiple different scopes/items, this will be called * with the enabled scopes/items. * If the data-provider supports only one scope/item, this can be ignored. * The lists contains all scopes/items, even those that are not supported by this provider. * */ virtual void enableData(const QStringList& items, const QStringList& scopes); }; /** * Try parsing string according to "path_to_file":"line number" template. "line number" may be empty. * @param from Source string * @param path Set to parsed path to file, or left unchanged if @p from doesn't match the template. May refer to the same object as @p from * @param lineNumber Set to parsed line number, zero if "line number" is empty or left unchanged if @p from doesn't match the template. * @return Whether @p from did match the expected template. * */ bool KDEVPLATFORMLANGUAGE_EXPORT extractLineNumber(const QString& from, QString& path, uint& lineNumber); } #endif diff --git a/plugins/contextbrowser/contextbrowser.cpp b/plugins/contextbrowser/contextbrowser.cpp index 17d6db849d..e7c3f6b1b8 100644 --- a/plugins/contextbrowser/contextbrowser.cpp +++ b/plugins/contextbrowser/contextbrowser.cpp @@ -1,1614 +1,1608 @@ /* * This file is part of KDevelop * * Copyright 2007 David Nolden * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "contextbrowser.h" #include "contextbrowserview.h" #include "browsemanager.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include using KTextEditor::Attribute; using KTextEditor::View; using namespace KDevelop; // Helper that follows the QObject::parent() chain, and returns the highest widget that has no parent. QWidget* masterWidget(QWidget* w) { while (w && w->parent() && qobject_cast(w->parent())) w = qobject_cast(w->parent()); return w; } namespace { const unsigned int highlightingTimeout = 150; const float highlightingZDepth = -5000; const int maxHistoryLength = 30; // Helper that determines the context to use for highlighting at a specific position DUContext* contextForHighlightingAt(const KTextEditor::Cursor& position, TopDUContext* topContext) { DUContext* ctx = topContext->findContextAt(topContext->transformToLocalRevision(position)); while (ctx && ctx->parentContext() && (ctx->type() == DUContext::Template || ctx->type() == DUContext::Helper || ctx->localScopeIdentifier().isEmpty())) { ctx = ctx->parentContext(); } return ctx; } ///Duchain must be locked DUContext* contextAt(const QUrl& url, KTextEditor::Cursor cursor) { TopDUContext* topContext = DUChainUtils::standardContextForUrl(url); if (!topContext) return nullptr; return contextForHighlightingAt(KTextEditor::Cursor(cursor), topContext); } DeclarationPointer cursorDeclaration() { KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView(); if (!view) { return DeclarationPointer(); } DUChainReadLocker lock; Declaration* decl = DUChainUtils::declarationForDefinition(DUChainUtils::itemUnderCursor(view->document()->url(), KTextEditor::Cursor( view->cursorPosition())).declaration); return DeclarationPointer(decl); } } class ContextBrowserViewFactory : public KDevelop::IToolViewFactory { public: explicit ContextBrowserViewFactory(ContextBrowserPlugin* plugin) : m_plugin(plugin) {} QWidget* create(QWidget* parent = nullptr) override { auto* ret = new ContextBrowserView(m_plugin, parent); return ret; } Qt::DockWidgetArea defaultPosition() override { return Qt::BottomDockWidgetArea; } QString id() const override { return QStringLiteral("org.kdevelop.ContextBrowser"); } private: ContextBrowserPlugin* m_plugin; }; KXMLGUIClient* ContextBrowserPlugin::createGUIForMainWindow(Sublime::MainWindow* window) { m_browseManager = new BrowseManager(this); KXMLGUIClient* ret = KDevelop::IPlugin::createGUIForMainWindow(window); connect( ICore::self()->documentController(), &IDocumentController::documentJumpPerformed, this, &ContextBrowserPlugin::documentJumpPerformed); m_previousButton = new QToolButton(); m_previousButton->setToolTip(i18n("Go back in context history")); m_previousButton->setAutoRaise(true); m_previousButton->setPopupMode(QToolButton::MenuButtonPopup); m_previousButton->setIcon(QIcon::fromTheme(QStringLiteral("go-previous"))); m_previousButton->setEnabled(false); m_previousButton->setFocusPolicy(Qt::NoFocus); m_previousMenu = new QMenu(m_previousButton); m_previousButton->setMenu(m_previousMenu); connect(m_previousButton.data(), &QToolButton::clicked, this, &ContextBrowserPlugin::historyPrevious); connect(m_previousMenu.data(), &QMenu::aboutToShow, this, &ContextBrowserPlugin::previousMenuAboutToShow); m_nextButton = new QToolButton(); m_nextButton->setToolTip(i18n("Go forward in context history")); m_nextButton->setAutoRaise(true); m_nextButton->setPopupMode(QToolButton::MenuButtonPopup); m_nextButton->setIcon(QIcon::fromTheme(QStringLiteral("go-next"))); m_nextButton->setEnabled(false); m_nextButton->setFocusPolicy(Qt::NoFocus); m_nextMenu = new QMenu(m_nextButton); m_nextButton->setMenu(m_nextMenu); connect(m_nextButton.data(), &QToolButton::clicked, this, &ContextBrowserPlugin::historyNext); connect(m_nextMenu.data(), &QMenu::aboutToShow, this, &ContextBrowserPlugin::nextMenuAboutToShow); auto* quickOpen = KDevelop::ICore::self()->pluginController()->extensionForPlugin(QStringLiteral( "org.kdevelop.IQuickOpen")); if (quickOpen) { m_outlineLine = quickOpen->createQuickOpenLine(QStringList(), QStringList() << i18n( "Outline"), IQuickOpen::Outline); m_outlineLine->setDefaultText(i18n("Outline...")); m_outlineLine->setToolTip(i18n("Navigate outline of active document, click to browse.")); } connect(m_browseManager, &BrowseManager::startDelayedBrowsing, this, &ContextBrowserPlugin::startDelayedBrowsing); connect(m_browseManager, &BrowseManager::stopDelayedBrowsing, this, &ContextBrowserPlugin::stopDelayedBrowsing); connect(m_browseManager, &BrowseManager::invokeAction, this, &ContextBrowserPlugin::invokeAction); m_toolbarWidget = toolbarWidgetForMainWindow(window); m_toolbarWidgetLayout = new QHBoxLayout; m_toolbarWidgetLayout->setSizeConstraint(QLayout::SetMaximumSize); m_previousButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_nextButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_toolbarWidgetLayout->setMargin(0); m_toolbarWidgetLayout->addWidget(m_previousButton); if (m_outlineLine) { m_toolbarWidgetLayout->addWidget(m_outlineLine); m_outlineLine->setMaximumWidth(600); connect(ICore::self()->documentController(), &IDocumentController::documentClosed, m_outlineLine.data(), &IQuickOpenLine::clear); } m_toolbarWidgetLayout->addWidget(m_nextButton); if (m_toolbarWidget->children().isEmpty()) m_toolbarWidget->setLayout(m_toolbarWidgetLayout); connect(ICore::self()->documentController(), &IDocumentController::documentActivated, this, &ContextBrowserPlugin::documentActivated); return ret; } void ContextBrowserPlugin::createActionsForMainWindow(Sublime::MainWindow* window, QString& xmlFile, KActionCollection& actions) { xmlFile = QStringLiteral("kdevcontextbrowser.rc"); QAction* sourceBrowseMode = actions.addAction(QStringLiteral("source_browse_mode")); sourceBrowseMode->setText(i18n("Source &Browse Mode")); sourceBrowseMode->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up"))); sourceBrowseMode->setCheckable(true); connect(sourceBrowseMode, &QAction::triggered, m_browseManager, &BrowseManager::setBrowsing); QAction* previousContext = actions.addAction(QStringLiteral("previous_context")); previousContext->setText(i18n("&Previous Visited Context")); previousContext->setIcon(QIcon::fromTheme(QStringLiteral("go-previous-context"))); actions.setDefaultShortcut(previousContext, Qt::META | Qt::Key_Left); QObject::connect(previousContext, &QAction::triggered, this, &ContextBrowserPlugin::previousContextShortcut); QAction* nextContext = actions.addAction(QStringLiteral("next_context")); nextContext->setText(i18n("&Next Visited Context")); nextContext->setIcon(QIcon::fromTheme(QStringLiteral("go-next-context"))); actions.setDefaultShortcut(nextContext, Qt::META | Qt::Key_Right); QObject::connect(nextContext, &QAction::triggered, this, &ContextBrowserPlugin::nextContextShortcut); QAction* previousUse = actions.addAction(QStringLiteral("previous_use")); previousUse->setText(i18n("&Previous Use")); previousUse->setIcon(QIcon::fromTheme(QStringLiteral("go-previous-use"))); actions.setDefaultShortcut(previousUse, Qt::META | Qt::SHIFT | Qt::Key_Left); QObject::connect(previousUse, &QAction::triggered, this, &ContextBrowserPlugin::previousUseShortcut); QAction* nextUse = actions.addAction(QStringLiteral("next_use")); nextUse->setText(i18n("&Next Use")); nextUse->setIcon(QIcon::fromTheme(QStringLiteral("go-next-use"))); actions.setDefaultShortcut(nextUse, Qt::META | Qt::SHIFT | Qt::Key_Right); QObject::connect(nextUse, &QAction::triggered, this, &ContextBrowserPlugin::nextUseShortcut); auto* outline = new QWidgetAction(this); outline->setText(i18n("Context Browser")); QWidget* w = toolbarWidgetForMainWindow(window); w->setHidden(false); outline->setDefaultWidget(w); actions.addAction(QStringLiteral("outline_line"), outline); // Add to the actioncollection so one can set global shortcuts for the action actions.addAction(QStringLiteral("find_uses"), m_findUses); } void ContextBrowserPlugin::nextContextShortcut() { // TODO: cleanup historyNext(); } void ContextBrowserPlugin::previousContextShortcut() { // TODO: cleanup historyPrevious(); } K_PLUGIN_FACTORY_WITH_JSON(ContextBrowserFactory, "kdevcontextbrowser.json", registerPlugin(); ) ContextBrowserPlugin::ContextBrowserPlugin(QObject* parent, const QVariantList&) : KDevelop::IPlugin(QStringLiteral("kdevcontextbrowser"), parent) , m_viewFactory(new ContextBrowserViewFactory(this)) , m_nextHistoryIndex(0) , m_textHintProvider(this) { qRegisterMetaType("KDevelop::IndexedDeclaration"); core()->uiController()->addToolView(i18n("Code Browser"), m_viewFactory); connect( core()->documentController(), &IDocumentController::textDocumentCreated, this, &ContextBrowserPlugin::textDocumentCreated); connect(DUChain::self(), &DUChain::updateReady, this, &ContextBrowserPlugin::updateReady); connect(ColorCache::self(), &ColorCache::colorsGotChanged, this, &ContextBrowserPlugin::colorSetupChanged); connect(DUChain::self(), &DUChain::declarationSelected, this, &ContextBrowserPlugin::declarationSelectedInUI); m_updateTimer = new QTimer(this); m_updateTimer->setSingleShot(true); connect(m_updateTimer, &QTimer::timeout, this, &ContextBrowserPlugin::updateViews); //Needed global action for the context-menu extensions m_findUses = new QAction(i18n("Find Uses"), this); connect(m_findUses, &QAction::triggered, this, &ContextBrowserPlugin::findUses); } ContextBrowserPlugin::~ContextBrowserPlugin() { ///TODO: QObject inheritance should suffice? delete m_nextMenu; delete m_previousMenu; delete m_toolbarWidgetLayout; delete m_previousButton; delete m_outlineLine; delete m_nextButton; } void ContextBrowserPlugin::unload() { core()->uiController()->removeToolView(m_viewFactory); } KDevelop::ContextMenuExtension ContextBrowserPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent) { KDevelop::ContextMenuExtension menuExt = KDevelop::IPlugin::contextMenuExtension(context, parent); auto* codeContext = dynamic_cast(context); if (!codeContext) return menuExt; DUChainReadLocker lock(DUChain::lock()); if (!codeContext->declaration().data()) return menuExt; menuExt.addAction(KDevelop::ContextMenuExtension::NavigationGroup, m_findUses); return menuExt; } void ContextBrowserPlugin::showUses(const DeclarationPointer& declaration) { QMetaObject::invokeMethod(this, "showUsesDelayed", Qt::QueuedConnection, Q_ARG(KDevelop::DeclarationPointer, declaration)); } void ContextBrowserPlugin::showUsesDelayed(const DeclarationPointer& declaration) { DUChainReadLocker lock; Declaration* decl = declaration.data(); if (!decl) { return; } QWidget* toolView = ICore::self()->uiController()->findToolView(i18n( "Code Browser"), m_viewFactory, KDevelop::IUiController::CreateAndRaise); if (!toolView) { return; } auto* view = dynamic_cast(toolView); Q_ASSERT(view); view->allowLockedUpdate(); view->setDeclaration(decl, decl->topContext(), true); //We may get deleted while the call to acceptLink, so make sure we don't crash in that case QPointer widget = dynamic_cast(view->navigationWidget()); if (widget && widget->context()) { auto nextContext = widget->context()->execute( NavigationAction(declaration, KDevelop::NavigationAction::ShowUses)); if (widget) { widget->setContext(nextContext); } } } void ContextBrowserPlugin::findUses() { showUses(cursorDeclaration()); } ContextBrowserHintProvider::ContextBrowserHintProvider(ContextBrowserPlugin* plugin) : m_plugin(plugin) { } QString ContextBrowserHintProvider::textHint(View* view, const KTextEditor::Cursor& cursor) { m_plugin->m_mouseHoverCursor = KTextEditor::Cursor(cursor); if (!view) { qCWarning(PLUGIN_CONTEXTBROWSER) << "could not cast to view"; } else { m_plugin->m_mouseHoverDocument = view->document()->url(); m_plugin->m_updateViews << view; } m_plugin->m_updateTimer->start(1); // triggers updateViews() m_plugin->showToolTip(view, cursor); return QString(); } void ContextBrowserPlugin::stopDelayedBrowsing() { hideToolTip(); } void ContextBrowserPlugin::invokeAction(int index) { if (!m_currentNavigationWidget) return; auto navigationWidget = qobject_cast(m_currentNavigationWidget); if (!navigationWidget) return; // TODO: Add API in AbstractNavigation{Widget,Context}? QMetaObject::invokeMethod(navigationWidget->context().data(), "executeAction", Q_ARG(int, index)); } void ContextBrowserPlugin::startDelayedBrowsing(KTextEditor::View* view) { if (!m_currentToolTip) { showToolTip(view, view->cursorPosition()); } } void ContextBrowserPlugin::hideToolTip() { if (m_currentToolTip) { m_currentToolTip->deleteLater(); m_currentToolTip = nullptr; m_currentNavigationWidget = nullptr; m_currentToolTipProblems.clear(); m_currentToolTipDeclaration = {}; } } static QVector findProblemsUnderCursor(TopDUContext* topContext, KTextEditor::Cursor position, KTextEditor::Range& handleRange) { QVector problems; handleRange = KTextEditor::Range::invalid(); const auto modelsData = ICore::self()->languageController()->problemModelSet()->models(); for (const auto& modelData : modelsData) { foreach (const auto& problem, modelData.model->problems(topContext->url())) { DocumentRange problemRange = problem->finalLocation(); if (problemRange.contains(position) || (problemRange.isEmpty() && problemRange.boundaryAtCursor(position))) { problems += problem; // first? if (!handleRange.isValid()) { handleRange = problemRange; } else { handleRange.confineToRange(problemRange); } } } } return problems; } static QVector findProblemsCloseToCursor(const TopDUContext* topContext, KTextEditor::Cursor position, const KTextEditor::View* view, KTextEditor::Range& handleRange) { handleRange = KTextEditor::Range::invalid(); QVector allProblems; const auto modelsData = ICore::self()->languageController()->problemModelSet()->models(); for (const auto& modelData : modelsData) { const auto problems = modelData.model->problems(topContext->url()); allProblems.reserve(allProblems.size() + problems.size()); for (const auto& problem : problems) { allProblems += problem; } } if (allProblems.isEmpty()) return allProblems; std::sort(allProblems.begin(), allProblems.end(), [position](const KDevelop::IProblem::Ptr& a, const KDevelop::IProblem::Ptr& b) { const auto aRange = a->finalLocation(); const auto bRange = b->finalLocation(); const auto aLineDistance = qMin(qAbs(aRange.start().line() - position.line()), qAbs(aRange.end().line() - position.line())); const auto bLineDistance = qMin(qAbs(bRange.start().line() - position.line()), qAbs(bRange.end().line() - position.line())); if (aLineDistance != bLineDistance) { return aLineDistance < bLineDistance; } if (aRange.start().line() == bRange.start().line()) { return qAbs(aRange.start().column() - position.column()) < qAbs(bRange.start().column() - position.column()); } return qAbs(aRange.end().column() - position.column()) < qAbs(bRange.end().column() - position.column()); }); QVector closestProblems; // Show problems, located on the same line foreach (auto problem, allProblems) { auto r = problem->finalLocation(); if (r.onSingleLine() && r.start().line() == position.line()) closestProblems += problem; else break; } // If not, only show it in case there's only whitespace // between the current cursor position and the problem line if (closestProblems.isEmpty()) { foreach (auto problem, allProblems) { auto r = problem->finalLocation(); KTextEditor::Range dist; KTextEditor::Cursor bound(r.start().line(), 0); if (position < r.start()) dist = KTextEditor::Range(position, bound); else { bound.setLine(r.end().line() + 1); dist = KTextEditor::Range(bound, position); } if (view->document()->text(dist).trimmed().isEmpty()) closestProblems += problem; else break; } } if (!closestProblems.isEmpty()) { auto it = closestProblems.constBegin(); handleRange = (*it)->finalLocation(); ++it; for (auto end = closestProblems.constEnd(); it != end; ++it) { handleRange.confineToRange((*it)->finalLocation()); } } return closestProblems; } QWidget* ContextBrowserPlugin::navigationWidgetForPosition(KTextEditor::View* view, KTextEditor::Cursor position, KTextEditor::Range& itemRange) { QUrl viewUrl = view->document()->url(); const auto languages = ICore::self()->languageController()->languagesForUrl(viewUrl); DUChainReadLocker lock(DUChain::lock()); for (const auto language : languages) { auto widget = language->specialLanguageObjectNavigationWidget(viewUrl, position); auto navigationWidget = qobject_cast(widget.first); if (navigationWidget) { itemRange = widget.second; return navigationWidget; } } TopDUContext* topContext = DUChainUtils::standardContextForUrl(view->document()->url()); QVector problems; if (topContext) { // first pass: find problems under the cursor problems = findProblemsUnderCursor(topContext, position, itemRange); } const auto itemUnderCursor = DUChainUtils::itemUnderCursor(viewUrl, position); auto declUnderCursor = itemUnderCursor.declaration; Declaration* decl = DUChainUtils::declarationForDefinition(declUnderCursor); if (decl && decl->kind() == Declaration::Alias) { auto* alias = dynamic_cast(decl); Q_ASSERT(alias); decl = alias->aliasedDeclaration().declaration(); } // Return nullptr if the correct contents are already shown. if (m_currentToolTip && problems == m_currentToolTipProblems && (!decl || IndexedDeclaration(decl) == m_currentToolTipDeclaration)) { return nullptr; } // Remember current state. m_currentToolTipProblems = problems; if (decl) { m_currentToolTipDeclaration = IndexedDeclaration(decl); } AbstractNavigationWidget* problemWidget = nullptr; if (!problems.isEmpty()) { problemWidget = new AbstractNavigationWidget; auto context = new ProblemNavigationContext(problems); context->setTopContext(TopDUContextPointer(topContext)); problemWidget->setContext(NavigationContextPointer(context)); } - QWidget* declWidget = nullptr; + AbstractNavigationWidget* declWidget = nullptr; if (decl) { if (itemRange.isValid()) { itemRange.expandToRange(itemUnderCursor.range); } else { itemRange = itemUnderCursor.range; } declWidget = decl->context()->createNavigationWidget(decl, DUChainUtils::standardContextForUrl(viewUrl)); } if (problemWidget && declWidget) { - QWidget* combinedWidget = new QWidget(); - QVBoxLayout* layout = new QVBoxLayout(); - layout->setContentsMargins(2, 2, 2, 2); - layout->setSpacing(0); - layout->addWidget(problemWidget); - layout->addWidget(declWidget); - combinedWidget->setLayout(layout); + auto* combinedWidget = new QuickOpenEmbeddedWidgetCombiner; + combinedWidget->layout()->addWidget(problemWidget); + combinedWidget->layout()->addWidget(declWidget); return combinedWidget; } else if (problemWidget) { return problemWidget; } else if (declWidget) { return declWidget; } if (topContext) { // second pass: find closest problem to the cursor problems = findProblemsCloseToCursor(topContext, position, view, itemRange); if (!problems.isEmpty()) { if (problems == m_currentToolTipProblems && m_currentToolTip) { return nullptr; } m_currentToolTipProblems = problems; auto widget = new AbstractNavigationWidget; // since the problem is not under cursor: show location widget->setContext(NavigationContextPointer(new ProblemNavigationContext(problems, ProblemNavigationContext:: ShowLocation))); return widget; } } return nullptr; } void ContextBrowserPlugin::showToolTip(KTextEditor::View* view, KTextEditor::Cursor position) { ContextBrowserView* contextView = browserViewForWidget(view); if (contextView && contextView->isVisible() && !contextView->isLocked()) return; // If the context-browser view is visible, it will care about updating by itself KTextEditor::Range itemRange = KTextEditor::Range::invalid(); auto navigationWidget = navigationWidgetForPosition(view, position, itemRange); if (navigationWidget) { // If we have an invisible context-view, assign the tooltip navigation-widget to it. // If the user makes the context-view visible, it will instantly contain the correct widget. if (contextView && !contextView->isLocked()) contextView->setNavigationWidget(navigationWidget); if (m_currentToolTip) { m_currentToolTip->deleteLater(); m_currentToolTip = nullptr; m_currentNavigationWidget = nullptr; } KDevelop::NavigationToolTip* tooltip = new KDevelop::NavigationToolTip(view, view->mapToGlobal(view->cursorToCoordinate(position)) + QPoint(20, 40), navigationWidget); if (!itemRange.isValid()) { qCWarning(PLUGIN_CONTEXTBROWSER) << "Got navigationwidget with invalid itemrange"; itemRange = KTextEditor::Range(position, 0); } tooltip->setHandleRect(KTextEditorHelpers::itemBoundingRect(view, itemRange)); tooltip->resize(navigationWidget->sizeHint() + QSize(10, 10)); QObject::connect(view, &KTextEditor::View::verticalScrollPositionChanged, this, &ContextBrowserPlugin::hideToolTip); QObject::connect(view, &KTextEditor::View::horizontalScrollPositionChanged, this, &ContextBrowserPlugin::hideToolTip); qCDebug(PLUGIN_CONTEXTBROWSER) << "tooltip size" << tooltip->size(); m_currentToolTip = tooltip; m_currentNavigationWidget = navigationWidget; ActiveToolTip::showToolTip(tooltip); if (!navigationWidget->property("DoNotCloseOnCursorMove").toBool()) { connect(view, &View::cursorPositionChanged, this, &ContextBrowserPlugin::hideToolTip, Qt::UniqueConnection); } else { disconnect(view, &View::cursorPositionChanged, this, &ContextBrowserPlugin::hideToolTip); } } else { qCDebug(PLUGIN_CONTEXTBROWSER) << "not showing tooltip, no navigation-widget"; } } void ContextBrowserPlugin::clearMouseHover() { m_mouseHoverCursor = KTextEditor::Cursor::invalid(); m_mouseHoverDocument.clear(); } Attribute::Ptr ContextBrowserPlugin::highlightedUseAttribute(KTextEditor::View* view) const { if (!m_highlightAttribute) { m_highlightAttribute = Attribute::Ptr(new Attribute()); m_highlightAttribute->setDefaultStyle(KTextEditor::dsNormal); m_highlightAttribute->setForeground(m_highlightAttribute->selectedForeground()); m_highlightAttribute->setBackgroundFillWhitespace(true); auto iface = qobject_cast(view); auto background = iface->configValue(QStringLiteral("search-highlight-color")).value(); m_highlightAttribute->setBackground(background); } return m_highlightAttribute; } void ContextBrowserPlugin::colorSetupChanged() { m_highlightAttribute = Attribute::Ptr(); } Attribute::Ptr ContextBrowserPlugin::highlightedSpecialObjectAttribute(KTextEditor::View* view) const { return highlightedUseAttribute(view); } void ContextBrowserPlugin::addHighlight(View* view, KDevelop::Declaration* decl) { if (!view || !decl) { qCDebug(PLUGIN_CONTEXTBROWSER) << "invalid view/declaration"; return; } ViewHighlights& highlights(m_highlightedRanges[view]); KDevelop::DUChainReadLocker lock; // Highlight the declaration highlights.highlights << decl->createRangeMoving(); highlights.highlights.back()->setAttribute(highlightedUseAttribute(view)); highlights.highlights.back()->setZDepth(highlightingZDepth); // Highlight uses { const auto currentRevisionUses = decl->usesCurrentRevision(); for (auto fileIt = currentRevisionUses.constBegin(); fileIt != currentRevisionUses.constEnd(); ++fileIt) { for (auto useIt = (*fileIt).constBegin(); useIt != (*fileIt).constEnd(); ++useIt) { highlights.highlights << PersistentMovingRange::Ptr(new PersistentMovingRange(*useIt, fileIt.key())); highlights.highlights.back()->setAttribute(highlightedUseAttribute(view)); highlights.highlights.back()->setZDepth(highlightingZDepth); } } } if (FunctionDefinition* def = FunctionDefinition::definition(decl)) { highlights.highlights << def->createRangeMoving(); highlights.highlights.back()->setAttribute(highlightedUseAttribute(view)); highlights.highlights.back()->setZDepth(highlightingZDepth); } } Declaration* ContextBrowserPlugin::findDeclaration(View* view, const KTextEditor::Cursor& position, bool mouseHighlight) { Q_UNUSED(mouseHighlight); Declaration* foundDeclaration = nullptr; if (m_useDeclaration.data()) { foundDeclaration = m_useDeclaration.data(); } else { //If we haven't found a special language object, search for a use/declaration and eventually highlight it foundDeclaration = DUChainUtils::declarationForDefinition(DUChainUtils::itemUnderCursor( view->document()->url(), position).declaration); if (foundDeclaration && foundDeclaration->kind() == Declaration::Alias) { auto* alias = dynamic_cast(foundDeclaration); Q_ASSERT(alias); DUChainReadLocker lock; foundDeclaration = alias->aliasedDeclaration().declaration(); } } return foundDeclaration; } ContextBrowserView* ContextBrowserPlugin::browserViewForWidget(QWidget* widget) { foreach (ContextBrowserView* contextView, m_views) { if (masterWidget(contextView) == masterWidget(widget)) { return contextView; } } return nullptr; } void ContextBrowserPlugin::updateForView(View* view) { bool allowHighlight = true; if (view->selection()) { // If something is selected, we unhighlight everything, so that we don't conflict with the // kate plugin that highlights occurrences of the selected string, and also to reduce the // overall amount of concurrent highlighting. allowHighlight = false; } if (m_highlightedRanges[view].keep) { m_highlightedRanges[view].keep = false; return; } // Clear all highlighting m_highlightedRanges.clear(); // Re-highlight ViewHighlights& highlights = m_highlightedRanges[view]; QUrl url = view->document()->url(); IDocument* activeDoc = core()->documentController()->activeDocument(); bool mouseHighlight = (url == m_mouseHoverDocument) && (m_mouseHoverCursor.isValid()); bool shouldUpdateBrowser = (mouseHighlight || (view == ICore::self()->documentController()->activeTextDocumentView() && activeDoc && activeDoc->textDocument() == view->document())); KTextEditor::Cursor highlightPosition; if (mouseHighlight) highlightPosition = m_mouseHoverCursor; else highlightPosition = KTextEditor::Cursor(view->cursorPosition()); ///Pick a language ILanguageSupport* language = nullptr; if (ICore::self()->languageController()->languagesForUrl(url).isEmpty()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "found no language for document" << url; return; } else { language = ICore::self()->languageController()->languagesForUrl(url).front(); } ///Check whether there is a special language object to highlight (for example a macro) KTextEditor::Range specialRange = language->specialLanguageObjectRange(url, highlightPosition); ContextBrowserView* updateBrowserView = shouldUpdateBrowser ? browserViewForWidget(view) : nullptr; if (specialRange.isValid()) { // Highlight a special language object if (allowHighlight) { highlights.highlights << PersistentMovingRange::Ptr(new PersistentMovingRange(specialRange, IndexedString(url))); highlights.highlights.back()->setAttribute(highlightedSpecialObjectAttribute(view)); highlights.highlights.back()->setZDepth(highlightingZDepth); } if (updateBrowserView) updateBrowserView->setSpecialNavigationWidget(language->specialLanguageObjectNavigationWidget(url, highlightPosition).first); } else { KDevelop::DUChainReadLocker lock(DUChain::lock(), 100); if (!lock.locked()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "Failed to lock du-chain in time"; return; } TopDUContext* topContext = DUChainUtils::standardContextForUrl(view->document()->url()); if (!topContext) return; DUContext* ctx = contextForHighlightingAt(highlightPosition, topContext); if (!ctx) return; //Only update the history if this context is around the text cursor if (core()->documentController()->activeDocument() && highlightPosition == KTextEditor::Cursor(view->cursorPosition()) && view->document() == core()->documentController()->activeDocument()->textDocument()) { updateHistory(ctx, highlightPosition); } Declaration* foundDeclaration = findDeclaration(view, highlightPosition, mouseHighlight); if (foundDeclaration) { m_lastHighlightedDeclaration = highlights.declaration = IndexedDeclaration(foundDeclaration); if (allowHighlight) addHighlight(view, foundDeclaration); if (updateBrowserView) updateBrowserView->setDeclaration(foundDeclaration, topContext); } else { if (updateBrowserView) updateBrowserView->setContext(ctx); } } } void ContextBrowserPlugin::updateViews() { foreach (View* view, m_updateViews) { updateForView(view); } m_updateViews.clear(); m_useDeclaration = IndexedDeclaration(); } void ContextBrowserPlugin::declarationSelectedInUI(const DeclarationPointer& decl) { m_useDeclaration = IndexedDeclaration(decl.data()); KTextEditor::View* view = core()->documentController()->activeTextDocumentView(); if (view) m_updateViews << view; if (!m_updateViews.isEmpty()) m_updateTimer->start(highlightingTimeout); // triggers updateViews() } void ContextBrowserPlugin::updateReady(const IndexedString& file, const ReferencedTopDUContext& /*topContext*/) { const auto url = file.toUrl(); for (QMap::iterator it = m_highlightedRanges.begin(); it != m_highlightedRanges.end(); ++it) { if (it.key()->document()->url() == url) { if (!m_updateViews.contains(it.key())) { qCDebug(PLUGIN_CONTEXTBROWSER) << "adding view for update"; m_updateViews << it.key(); // Don't change the highlighted declaration after finished parse-jobs (*it).keep = true; } } } if (!m_updateViews.isEmpty()) m_updateTimer->start(highlightingTimeout); } void ContextBrowserPlugin::textDocumentCreated(KDevelop::IDocument* document) { Q_ASSERT(document->textDocument()); connect(document->textDocument(), &KTextEditor::Document::viewCreated, this, &ContextBrowserPlugin::viewCreated); foreach (View* view, document->textDocument()->views()) viewCreated(document->textDocument(), view); } void ContextBrowserPlugin::documentActivated(IDocument* doc) { if (m_outlineLine) m_outlineLine->clear(); if (View* view = doc->activeTextView()) { cursorPositionChanged(view, view->cursorPosition()); } } void ContextBrowserPlugin::viewDestroyed(QObject* obj) { m_highlightedRanges.remove(static_cast(obj)); m_updateViews.remove(static_cast(obj)); } void ContextBrowserPlugin::selectionChanged(View* view) { clearMouseHover(); m_updateViews.insert(view); m_updateTimer->start(highlightingTimeout / 2); // triggers updateViews() } void ContextBrowserPlugin::cursorPositionChanged(View* view, const KTextEditor::Cursor& newPosition) { const bool atInsertPosition = (view->document() == m_lastInsertionDocument && newPosition == m_lastInsertionPos); if (atInsertPosition) { //Do not update the highlighting while typing m_lastInsertionDocument = nullptr; m_lastInsertionPos = KTextEditor::Cursor(); } const auto viewHighlightsIt = m_highlightedRanges.find(view); if (viewHighlightsIt != m_highlightedRanges.end()) { viewHighlightsIt->keep = atInsertPosition; } clearMouseHover(); m_updateViews.insert(view); m_updateTimer->start(highlightingTimeout / 2); // triggers updateViews() } void ContextBrowserPlugin::textInserted(KTextEditor::Document* doc, const KTextEditor::Cursor& cursor, const QString& text) { m_lastInsertionDocument = doc; m_lastInsertionPos = cursor + KTextEditor::Cursor(0, text.size()); } void ContextBrowserPlugin::viewCreated(KTextEditor::Document*, View* v) { disconnect(v, &View::cursorPositionChanged, this, &ContextBrowserPlugin::cursorPositionChanged); ///Just to make sure that multiple connections don't happen connect(v, &View::cursorPositionChanged, this, &ContextBrowserPlugin::cursorPositionChanged); connect(v, &View::destroyed, this, &ContextBrowserPlugin::viewDestroyed); disconnect(v->document(), &KTextEditor::Document::textInserted, this, &ContextBrowserPlugin::textInserted); connect(v->document(), &KTextEditor::Document::textInserted, this, &ContextBrowserPlugin::textInserted); disconnect(v, &View::selectionChanged, this, &ContextBrowserPlugin::selectionChanged); auto* iface = dynamic_cast(v); if (!iface) return; iface->setTextHintDelay(highlightingTimeout); iface->registerTextHintProvider(&m_textHintProvider); } void ContextBrowserPlugin::registerToolView(ContextBrowserView* view) { m_views << view; } void ContextBrowserPlugin::previousUseShortcut() { switchUse(false); } void ContextBrowserPlugin::nextUseShortcut() { switchUse(true); } KTextEditor::Range cursorToRange(KTextEditor::Cursor cursor) { return KTextEditor::Range(cursor, cursor); } void ContextBrowserPlugin::switchUse(bool forward) { View* view = core()->documentController()->activeTextDocumentView(); if (view) { KTextEditor::Document* doc = view->document(); KDevelop::DUChainReadLocker lock(DUChain::lock()); KDevelop::TopDUContext* chosen = DUChainUtils::standardContextForUrl(doc->url()); if (chosen) { KTextEditor::Cursor cCurrent(view->cursorPosition()); KDevelop::CursorInRevision c = chosen->transformToLocalRevision(cCurrent); Declaration* decl = nullptr; //If we have a locked declaration, use that for jumping foreach (ContextBrowserView* view, m_views) { decl = view->lockedDeclaration().data(); ///@todo Somehow match the correct context-browser view if there is multiple if (decl) break; } if (!decl) //Try finding a declaration under the cursor decl = DUChainUtils::itemUnderCursor(doc->url(), cCurrent).declaration; if (decl && decl->kind() == Declaration::Alias) { auto* alias = dynamic_cast(decl); Q_ASSERT(alias); DUChainReadLocker lock; decl = alias->aliasedDeclaration().declaration(); } if (decl) { Declaration* target = nullptr; if (forward) //Try jumping from definition to declaration target = DUChainUtils::declarationForDefinition(decl, chosen); else if (decl->url().toUrl() == doc->url() && decl->range().contains(c)) //Try jumping from declaration to definition target = FunctionDefinition::definition(decl); if (target && target != decl) { KTextEditor::Cursor jumpTo = target->rangeInCurrentRevision().start(); QUrl document = target->url().toUrl(); lock.unlock(); core()->documentController()->openDocument(document, cursorToRange(jumpTo)); return; } else { //Always work with the declaration instead of the definition decl = DUChainUtils::declarationForDefinition(decl, chosen); } } if (!decl) { //Pick the last use we have highlighted decl = m_lastHighlightedDeclaration.data(); } if (decl) { KDevVarLengthArray usingFiles = DUChain::uses()->uses(decl->id()); if (DUChainUtils::contextHasUse(decl->topContext(), decl) && usingFiles.indexOf(decl->topContext()) == -1) usingFiles.insert(0, decl->topContext()); if (decl->range().contains(c) && decl->url() == chosen->url()) { //The cursor is directly on the declaration. Jump to the first or last use. if (!usingFiles.isEmpty()) { TopDUContext* top = (forward ? usingFiles[0] : usingFiles.back()).data(); if (top) { QVector useRanges = allUses(top, decl, true); std::sort(useRanges.begin(), useRanges.end()); if (!useRanges.isEmpty()) { QUrl url = top->url().toUrl(); KTextEditor::Range selectUse = chosen->transformFromLocalRevision( forward ? useRanges.first() : useRanges.back()); lock.unlock(); core()->documentController()->openDocument(url, cursorToRange(selectUse.start())); } } } return; } //Check whether we are within a use QVector localUses = allUses(chosen, decl, true); std::sort(localUses.begin(), localUses.end()); for (int a = 0; a < localUses.size(); ++a) { int nextUse = (forward ? a + 1 : a - 1); bool pick = localUses[a].contains(c); if (!pick && forward && a + 1 < localUses.size() && localUses[a].end <= c && localUses[a + 1].start > c) { //Special case: We aren't on a use, but we are jumping forward, and are behind this and the next use pick = true; } if (!pick && !forward && a - 1 >= 0 && c < localUses[a].start && c >= localUses[a - 1].end) { //Special case: We aren't on a use, but we are jumping backward, and are in front of this use, but behind the previous one pick = true; } if (!pick && a == 0 && c < localUses[a].start) { if (!forward) { //Will automatically jump to previous file } else { nextUse = 0; //We are before the first use, so jump to it. } pick = true; } if (!pick && a == localUses.size() - 1 && c >= localUses[a].end) { if (forward) { //Will automatically jump to next file } else { //We are behind the last use, but moving backward. So pick the last use. nextUse = a; } pick = true; } if (pick) { //Make sure we end up behind the use if (nextUse != a) while (forward && nextUse < localUses.size() && (localUses[nextUse].start <= localUses[a].end || localUses[nextUse].isEmpty())) ++nextUse; //Make sure we end up before the use if (nextUse != a) while (!forward && nextUse >= 0 && (localUses[nextUse].start >= localUses[a].start || localUses[nextUse].isEmpty())) --nextUse; //Jump to the next use qCDebug(PLUGIN_CONTEXTBROWSER) << "count of uses:" << localUses.size() << "nextUse" << nextUse; if (nextUse < 0 || nextUse == localUses.size()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "jumping to next file"; //Jump to the first use in the next using top-context int indexInFiles = usingFiles.indexOf(chosen); if (indexInFiles != -1) { int nextFile = (forward ? indexInFiles + 1 : indexInFiles - 1); qCDebug(PLUGIN_CONTEXTBROWSER) << "current file" << indexInFiles << "nextFile" << nextFile; if (nextFile < 0 || nextFile >= usingFiles.size()) { //Open the declaration, or the definition if (nextFile >= usingFiles.size()) { Declaration* definition = FunctionDefinition::definition(decl); if (definition) decl = definition; } QUrl u = decl->url().toUrl(); KTextEditor::Range range = decl->rangeInCurrentRevision(); range.setEnd(range.start()); lock.unlock(); core()->documentController()->openDocument(u, range); return; } else { TopDUContext* nextTop = usingFiles[nextFile].data(); QUrl u = nextTop->url().toUrl(); QVector nextTopUses = allUses(nextTop, decl, true); std::sort(nextTopUses.begin(), nextTopUses.end()); if (!nextTopUses.isEmpty()) { KTextEditor::Range range = chosen->transformFromLocalRevision( forward ? nextTopUses.front() : nextTopUses.back()); range.setEnd(range.start()); lock.unlock(); core()->documentController()->openDocument(u, range); } return; } } else { qCDebug(PLUGIN_CONTEXTBROWSER) << "not found own file in use list"; } } else { QUrl url = chosen->url().toUrl(); KTextEditor::Range range = chosen->transformFromLocalRevision(localUses[nextUse]); range.setEnd(range.start()); lock.unlock(); core()->documentController()->openDocument(url, range); return; } } } } } } } void ContextBrowserPlugin::unRegisterToolView(ContextBrowserView* view) { m_views.removeAll(view); } // history browsing QWidget* ContextBrowserPlugin::toolbarWidgetForMainWindow(Sublime::MainWindow* window) { //TODO: support multiple windows (if that ever gets revived) if (!m_toolbarWidget) { m_toolbarWidget = new QWidget(window); } return m_toolbarWidget; } void ContextBrowserPlugin::documentJumpPerformed(KDevelop::IDocument* newDocument, const KTextEditor::Cursor& newCursor, KDevelop::IDocument* previousDocument, const KTextEditor::Cursor& previousCursor) { DUChainReadLocker lock(DUChain::lock()); /*TODO: support multiple windows if that ever gets revived if(newDocument && newDocument->textDocument() && newDocument->textDocument()->activeView() && masterWidget(newDocument->textDocument()->activeView()) != masterWidget(this)) return; */ if (previousDocument && previousCursor.isValid()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "updating jump source"; DUContext* context = contextAt(previousDocument->url(), previousCursor); if (context) { updateHistory(context, KTextEditor::Cursor(previousCursor), true); } else { //We just want this place in the history m_history.resize(m_nextHistoryIndex); // discard forward history m_history.append(HistoryEntry(DocumentCursor(IndexedString(previousDocument->url()), KTextEditor::Cursor(previousCursor)))); ++m_nextHistoryIndex; } } qCDebug(PLUGIN_CONTEXTBROWSER) << "new doc: " << newDocument << " new cursor: " << newCursor; if (newDocument && newCursor.isValid()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "updating jump target"; DUContext* context = contextAt(newDocument->url(), newCursor); if (context) { updateHistory(context, KTextEditor::Cursor(newCursor), true); } else { //We just want this place in the history m_history.resize(m_nextHistoryIndex); // discard forward history m_history.append(HistoryEntry(DocumentCursor(IndexedString(newDocument->url()), KTextEditor::Cursor(newCursor)))); ++m_nextHistoryIndex; if (m_outlineLine) m_outlineLine->clear(); } } } void ContextBrowserPlugin::updateButtonState() { m_nextButton->setEnabled(m_nextHistoryIndex < m_history.size()); m_previousButton->setEnabled(m_nextHistoryIndex >= 2); } void ContextBrowserPlugin::historyNext() { if (m_nextHistoryIndex >= m_history.size()) { return; } openDocument(m_nextHistoryIndex); // opening the document at given position // will update the widget for us ++m_nextHistoryIndex; updateButtonState(); } void ContextBrowserPlugin::openDocument(int historyIndex) { Q_ASSERT_X(historyIndex >= 0, "openDocument", "negative history index"); Q_ASSERT_X(historyIndex < m_history.size(), "openDocument", "history index out of range"); DocumentCursor c = m_history[historyIndex].computePosition(); if (c.isValid() && !c.document.str().isEmpty()) { disconnect( ICore::self()->documentController(), &IDocumentController::documentJumpPerformed, this, &ContextBrowserPlugin::documentJumpPerformed); ICore::self()->documentController()->openDocument(c.document.toUrl(), c); connect( ICore::self()->documentController(), &IDocumentController::documentJumpPerformed, this, &ContextBrowserPlugin::documentJumpPerformed); KDevelop::DUChainReadLocker lock(KDevelop::DUChain::lock()); updateDeclarationListBox(m_history[historyIndex].context.data()); } } void ContextBrowserPlugin::historyPrevious() { if (m_nextHistoryIndex < 2) { return; } --m_nextHistoryIndex; openDocument(m_nextHistoryIndex - 1); // opening the document at given position // will update the widget for us updateButtonState(); } QString ContextBrowserPlugin::actionTextFor(int historyIndex) const { const HistoryEntry& entry = m_history.at(historyIndex); QString actionText = entry.context.data() ? entry.context.data()->scopeIdentifier(true).toString() : QString(); if (actionText.isEmpty()) actionText = entry.alternativeString; if (actionText.isEmpty()) actionText = QStringLiteral(""); actionText += QLatin1String(" @ "); QString fileName = entry.absoluteCursorPosition.document.toUrl().fileName(); actionText += QStringLiteral("%1:%2").arg(fileName).arg(entry.absoluteCursorPosition.line() + 1); return actionText; } /* inline QDebug operator<<(QDebug debug, const ContextBrowserPlugin::HistoryEntry &he) { DocumentCursor c = he.computePosition(); debug << "\n\tHistoryEntry " << c.line << " " << c.document.str(); return debug; } */ void ContextBrowserPlugin::nextMenuAboutToShow() { QList indices; indices.reserve(m_history.size() - m_nextHistoryIndex); for (int a = m_nextHistoryIndex; a < m_history.size(); ++a) { indices << a; } fillHistoryPopup(m_nextMenu, indices); } void ContextBrowserPlugin::previousMenuAboutToShow() { QList indices; indices.reserve(m_nextHistoryIndex - 1); for (int a = m_nextHistoryIndex - 2; a >= 0; --a) { indices << a; } fillHistoryPopup(m_previousMenu, indices); } void ContextBrowserPlugin::fillHistoryPopup(QMenu* menu, const QList& historyIndices) { menu->clear(); KDevelop::DUChainReadLocker lock(KDevelop::DUChain::lock()); for (int index : historyIndices) { QAction* action = new QAction(actionTextFor(index), menu); action->setData(index); menu->addAction(action); connect(action, &QAction::triggered, this, &ContextBrowserPlugin::actionTriggered); } } bool ContextBrowserPlugin::isPreviousEntry(KDevelop::DUContext* context, const KTextEditor::Cursor& /*position*/) const { if (m_nextHistoryIndex == 0) return false; Q_ASSERT(m_nextHistoryIndex <= m_history.count()); const HistoryEntry& he = m_history.at(m_nextHistoryIndex - 1); KDevelop::DUChainReadLocker lock(KDevelop::DUChain::lock()); // is this necessary?? Q_ASSERT(context); return IndexedDUContext(context) == he.context; } void ContextBrowserPlugin::updateHistory(KDevelop::DUContext* context, const KTextEditor::Cursor& position, bool force) { qCDebug(PLUGIN_CONTEXTBROWSER) << "updating history"; if (m_outlineLine && m_outlineLine->isVisible()) updateDeclarationListBox(context); if (!context || (!context->owner() && !force)) { return; //Only add history-entries for contexts that have owners, which in practice should be functions and classes //This keeps the history cleaner } if (isPreviousEntry(context, position)) { if (m_nextHistoryIndex) { HistoryEntry& he = m_history[m_nextHistoryIndex - 1]; he.setCursorPosition(position); } return; } else { // Append new history entry m_history.resize(m_nextHistoryIndex); // discard forward history m_history.append(HistoryEntry(IndexedDUContext(context), position)); ++m_nextHistoryIndex; updateButtonState(); if (m_history.size() > (maxHistoryLength + 5)) { m_history.remove(0, m_history.size() - maxHistoryLength); m_nextHistoryIndex = m_history.size(); } } } void ContextBrowserPlugin::updateDeclarationListBox(DUContext* context) { if (!context || !context->owner()) { qCDebug(PLUGIN_CONTEXTBROWSER) << "not updating box"; m_listUrl = IndexedString(); ///@todo Compute the context in the document here if (m_outlineLine) m_outlineLine->clear(); return; } Declaration* decl = context->owner(); m_listUrl = context->url(); Declaration* specialDecl = SpecializationStore::self().applySpecialization(decl, decl->topContext()); FunctionType::Ptr function = specialDecl->type(); QString text = specialDecl->qualifiedIdentifier().toString(); if (function) text += function->partToString(KDevelop::FunctionType::SignatureArguments); if (m_outlineLine && !m_outlineLine->hasFocus()) { m_outlineLine->setText(text); m_outlineLine->setCursorPosition(0); } qCDebug(PLUGIN_CONTEXTBROWSER) << "updated" << text; } void ContextBrowserPlugin::actionTriggered() { auto* action = qobject_cast(sender()); Q_ASSERT(action); Q_ASSERT(action->data().type() == QVariant::Int); int historyPosition = action->data().toInt(); // qCDebug(PLUGIN_CONTEXTBROWSER) << "history pos" << historyPosition << m_history.size() << m_history; if (historyPosition >= 0 && historyPosition < m_history.size()) { m_nextHistoryIndex = historyPosition + 1; openDocument(historyPosition); updateButtonState(); } } void ContextBrowserPlugin::doNavigate(NavigationActionType action) { auto* view = qobject_cast(sender()); if (!view) { qCWarning(PLUGIN_CONTEXTBROWSER) << "sender is not a view"; return; } KTextEditor::CodeCompletionInterface* iface = qobject_cast(view); if (!iface || iface->isCompletionActive()) return; // If code completion is active, the actions should be handled by the completion widget QWidget* widget = m_currentNavigationWidget.data(); if (!widget || !widget->isVisible()) { ContextBrowserView* contextView = browserViewForWidget(view); if (contextView) widget = contextView->navigationWidget(); } - if (widget) { - auto* navWidget = qobject_cast(widget); - if (navWidget) { - switch (action) { - case Accept: - navWidget->accept(); - break; - case Back: - navWidget->back(); - break; - case Left: - navWidget->previous(); - break; - case Right: - navWidget->next(); - break; - case Up: - navWidget->up(); - break; - case Down: - navWidget->down(); - break; - } + if (auto* navWidget = dynamic_cast(widget)) { + switch (action) { + case Accept: + navWidget->accept(); + break; + case Back: + navWidget->back(); + break; + case Left: + navWidget->previous(); + break; + case Right: + navWidget->next(); + break; + case Up: + navWidget->up(); + break; + case Down: + navWidget->down(); + break; } } } void ContextBrowserPlugin::navigateAccept() { doNavigate(Accept); } void ContextBrowserPlugin::navigateBack() { doNavigate(Back); } void ContextBrowserPlugin::navigateDown() { doNavigate(Down); } void ContextBrowserPlugin::navigateLeft() { doNavigate(Left); } void ContextBrowserPlugin::navigateRight() { doNavigate(Right); } void ContextBrowserPlugin::navigateUp() { doNavigate(Up); } //BEGIN HistoryEntry ContextBrowserPlugin::HistoryEntry::HistoryEntry(const KDevelop::DocumentCursor& pos) : absoluteCursorPosition(pos) { } ContextBrowserPlugin::HistoryEntry::HistoryEntry(IndexedDUContext ctx, const KTextEditor::Cursor& cursorPosition) : context(ctx) { //Use a position relative to the context setCursorPosition(cursorPosition); if (ctx.data()) alternativeString = ctx.data()->scopeIdentifier(true).toString(); if (!alternativeString.isEmpty()) alternativeString += i18n("(changed)"); //This is used when the context was deleted in between } DocumentCursor ContextBrowserPlugin::HistoryEntry::computePosition() const { KDevelop::DUChainReadLocker lock(KDevelop::DUChain::lock()); DocumentCursor ret; if (context.data()) { ret = DocumentCursor(context.data()->url(), relativeCursorPosition); ret.setLine(ret.line() + context.data()->range().start.line); } else { ret = absoluteCursorPosition; } return ret; } void ContextBrowserPlugin::HistoryEntry::setCursorPosition(const KTextEditor::Cursor& cursorPosition) { KDevelop::DUChainReadLocker lock(KDevelop::DUChain::lock()); if (context.data()) { absoluteCursorPosition = DocumentCursor(context.data()->url(), cursorPosition); relativeCursorPosition = cursorPosition; relativeCursorPosition.setLine(relativeCursorPosition.line() - context.data()->range().start.line); } } #include "contextbrowser.moc"