/* * 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 #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 KDevelop::ILanguage; using KTextEditor::Attribute; using KTextEditor::View; // 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 SimpleCursor& 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* getContextAt(const KUrl& url, KTextEditor::Cursor cursor) { TopDUContext* topContext = DUChainUtils::standardContextForUrl(url); if (!topContext) return 0; return contextForHighlightingAt(SimpleCursor(cursor), topContext); } DeclarationPointer cursorDeclaration() { IDocument* doc = ICore::self()->documentController()->activeDocument(); if (!doc) { return DeclarationPointer(); } KTextEditor::Document* textDoc = doc->textDocument(); if (!textDoc) { return DeclarationPointer(); } KTextEditor::View* view = textDoc->activeView(); if (!view) { return DeclarationPointer(); } DUChainReadLocker lock; Declaration *decl = DUChainUtils::declarationForDefinition(DUChainUtils::itemUnderCursor(doc->url(), SimpleCursor(view->cursorPosition()))); return DeclarationPointer(decl); } } class ContextBrowserViewFactory: public KDevelop::IToolViewFactory { public: ContextBrowserViewFactory(ContextBrowserPlugin *plugin): m_plugin(plugin) {} virtual QWidget* create(QWidget *parent = 0) { ContextBrowserView* ret = new ContextBrowserView(m_plugin, parent); return ret; } virtual Qt::DockWidgetArea defaultPosition() { return Qt::BottomDockWidgetArea; } virtual QString id() const { return "org.kdevelop.ContextBrowser"; } private: ContextBrowserPlugin *m_plugin; }; KXMLGUIClient* ContextBrowserPlugin::createGUIForMainWindow( Sublime::MainWindow* window ) { KXMLGUIClient* ret = KDevelop::IPlugin::createGUIForMainWindow( window ); m_browseManager = new BrowseManager(this); connect(ICore::self()->documentController(), SIGNAL(documentJumpPerformed(KDevelop::IDocument*,KTextEditor::Cursor,KDevelop::IDocument*,KTextEditor::Cursor)), this, SLOT(documentJumpPerformed(KDevelop::IDocument*,KTextEditor::Cursor,KDevelop::IDocument*,KTextEditor::Cursor))); m_previousButton = new QToolButton(); m_previousButton->setToolTip(i18n("Go back in context history")); m_previousButton->setPopupMode(QToolButton::MenuButtonPopup); m_previousButton->setIcon(KIcon("go-previous")); m_previousButton->setEnabled(false); m_previousButton->setFocusPolicy(Qt::NoFocus); m_previousMenu = new QMenu(); m_previousButton->setMenu(m_previousMenu); connect(m_previousButton, SIGNAL(clicked(bool)), this, SLOT(historyPrevious())); connect(m_previousMenu, SIGNAL(aboutToShow()), this, SLOT(previousMenuAboutToShow())); m_nextButton = new QToolButton(); m_nextButton->setToolTip(i18n("Go forward in context history")); m_nextButton->setPopupMode(QToolButton::MenuButtonPopup); m_nextButton->setIcon(KIcon("go-next")); m_nextButton->setEnabled(false); m_nextButton->setFocusPolicy(Qt::NoFocus); m_nextMenu = new QMenu(); m_nextButton->setMenu(m_nextMenu); connect(m_nextButton, SIGNAL(clicked(bool)), this, SLOT(historyNext())); connect(m_nextMenu, SIGNAL(aboutToShow()), this, SLOT(nextMenuAboutToShow())); m_browseButton = new QToolButton(); m_browseButton->setIcon(KIcon("games-hint")); m_browseButton->setToolTip(i18n("Enable/disable source browse mode")); m_browseButton->setWhatsThis(i18n("When this is enabled, you can browse the source-code by clicking in the editor.")); m_browseButton->setCheckable(true); m_browseButton->setFocusPolicy(Qt::NoFocus); connect(m_browseButton, SIGNAL(clicked(bool)), m_browseManager, SLOT(setBrowsing(bool))); IQuickOpen* quickOpen = KDevelop::ICore::self()->pluginController()->extensionForPlugin("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, SIGNAL(startDelayedBrowsing(KTextEditor::View*)), this, SLOT(startDelayedBrowsing(KTextEditor::View*))); connect(m_browseManager, SIGNAL(stopDelayedBrowsing()), this, SLOT(stopDelayedBrowsing())); 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_browseButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_toolbarWidgetLayout->setMargin(0); m_toolbarWidgetLayout->addWidget(m_previousButton); m_toolbarWidgetLayout->addWidget(m_outlineLine); m_outlineLine->setMaximumWidth(600); m_toolbarWidgetLayout->addWidget(m_nextButton); m_toolbarWidgetLayout->addWidget(m_browseButton); if(m_toolbarWidget->children().isEmpty()) m_toolbarWidget->setLayout(m_toolbarWidgetLayout); connect(ICore::self()->documentController(), SIGNAL(documentClosed(KDevelop::IDocument*)), m_outlineLine, SLOT(clear())); connect(ICore::self()->documentController(), SIGNAL(documentActivated(KDevelop::IDocument*)), this, SLOT(documentActivated(KDevelop::IDocument*))); return ret; } void ContextBrowserPlugin::createActionsForMainWindow(Sublime::MainWindow* window, QString& xmlFile, KActionCollection& actions) { xmlFile = "kdevcontextbrowser.rc" ; KAction* previousContext = actions.addAction("previous_context"); previousContext->setText( i18n("&Previous Visited Context") ); previousContext->setIcon( KIcon("go-previous-context" ) ); previousContext->setShortcut( Qt::META | Qt::Key_Left ); QObject::connect(previousContext, SIGNAL(triggered(bool)), this, SLOT(previousContextShortcut())); KAction* nextContext = actions.addAction("next_context"); nextContext->setText( i18n("&Next Visited Context") ); nextContext->setIcon( KIcon("go-next-context" ) ); nextContext->setShortcut( Qt::META | Qt::Key_Right ); QObject::connect(nextContext, SIGNAL(triggered(bool)), this, SLOT(nextContextShortcut())); KAction* previousUse = actions.addAction("previous_use"); previousUse->setText( i18n("&Previous Use") ); previousUse->setIcon( KIcon("go-previous-use") ); previousUse->setShortcut( Qt::META | Qt::SHIFT | Qt::Key_Left ); QObject::connect(previousUse, SIGNAL(triggered(bool)), this, SLOT(previousUseShortcut())); KAction* nextUse = actions.addAction("next_use"); nextUse->setText( i18n("&Next Use") ); nextUse->setIcon( KIcon("go-next-use") ); nextUse->setShortcut( Qt::META | Qt::SHIFT | Qt::Key_Right ); QObject::connect(nextUse, SIGNAL(triggered(bool)), this, SLOT(nextUseShortcut())); KAction* outline = actions.addAction("outline_line"); outline->setText(i18n("Context Browser")); QWidget* w = toolbarWidgetForMainWindow(window); w->setHidden(false); outline->setDefaultWidget(w); // Add to the actioncollection so one can set global shortcuts for the action actions.addAction("find_uses", m_findUses); } void ContextBrowserPlugin::nextContextShortcut() { // TODO: cleanup historyNext(); } void ContextBrowserPlugin::previousContextShortcut() { // TODO: cleanup historyPrevious(); } K_PLUGIN_FACTORY(ContextBrowserFactory, registerPlugin(); ) K_EXPORT_PLUGIN(ContextBrowserFactory(KAboutData("kdevcontextbrowser","kdevcontextbrowser",ki18n("Context Browser"), "0.1", ki18n("This plugin shows information about the current language context in a side view, and highlights relevant declarations and uses."), KAboutData::License_GPL))) ContextBrowserPlugin::ContextBrowserPlugin(QObject *parent, const QVariantList&) : KDevelop::IPlugin(ContextBrowserFactory::componentData(), parent) , m_viewFactory(new ContextBrowserViewFactory(this)) , m_nextHistoryIndex(0) { KDEV_USE_EXTENSION_INTERFACE( IContextBrowser ) core()->uiController()->addToolView(i18n("Code Browser"), m_viewFactory); connect( core()->documentController(), SIGNAL(textDocumentCreated(KDevelop::IDocument*)), this, SLOT(textDocumentCreated(KDevelop::IDocument*)) ); connect( core()->languageController()->backgroundParser(), SIGNAL(parseJobFinished(KDevelop::ParseJob*)), this, SLOT(parseJobFinished(KDevelop::ParseJob*))); connect( DUChain::self(), SIGNAL(declarationSelected(KDevelop::DeclarationPointer)), this, SLOT(declarationSelectedInUI(KDevelop::DeclarationPointer)) ); m_updateTimer = new QTimer(this); m_updateTimer->setSingleShot(true); connect( m_updateTimer, SIGNAL(timeout()), this, SLOT(updateViews()) ); //Needed global action for the context-menu extensions m_findUses = new KAction(i18n("Find Uses"), this); connect(m_findUses, SIGNAL(triggered(bool)), this, SLOT(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; delete m_browseButton; } void ContextBrowserPlugin::unload() { core()->uiController()->removeToolView(m_viewFactory); } KDevelop::ContextMenuExtension ContextBrowserPlugin::contextMenuExtension(KDevelop::Context* context) { KDevelop::ContextMenuExtension menuExt = KDevelop::IPlugin::contextMenuExtension( context ); KDevelop::DeclarationContext *codeContext = dynamic_cast(context); if (!codeContext) return menuExt; DUChainReadLocker lock(DUChain::lock()); if(!codeContext->declaration().data()) return menuExt; qRegisterMetaType("KDevelop::IndexedDeclaration"); menuExt.addAction(KDevelop::ContextMenuExtension::ExtensionGroup, 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; } ContextBrowserView* 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()) { NavigationContextPointer nextContext = widget->context()->execute( NavigationAction(declaration, KDevelop::NavigationAction::ShowUses)); if(widget) { widget->setContext( nextContext ); } } } void ContextBrowserPlugin::findUses() { showUses(cursorDeclaration()); } void ContextBrowserPlugin::textHintRequested(const KTextEditor::Cursor& cursor, QString&) { m_mouseHoverCursor = SimpleCursor(cursor); View* view = dynamic_cast(sender()); if(!view) { kWarning() << "could not cast to view"; }else{ m_mouseHoverDocument = view->document()->url(); m_updateViews << view; } m_updateTimer->start(1); // triggers updateViews() showToolTip(view, cursor); } void ContextBrowserPlugin::stopDelayedBrowsing() { if(m_currentToolTip) { m_currentToolTip->deleteLater(); m_currentToolTip = 0; m_currentNavigationWidget = 0; } } void ContextBrowserPlugin::startDelayedBrowsing(KTextEditor::View* view) { if(!m_currentToolTip) { showToolTip(view, view->cursorPosition()); } } void ContextBrowserPlugin::hideToolTip() { if(m_currentToolTip) { m_currentToolTip->deleteLater(); m_currentToolTip = 0; m_currentNavigationWidget = 0; } } // TODO: this is a hack, but Kate does not provide interface for this static int getLineHeight(KTextEditor::View* view, int curLine) { KTextEditor::Cursor c(curLine, 0); int currentHeight = view->cursorToCoordinate(c).y(); c.setLine(curLine + 1); if (view->cursorToCoordinate(c).y() < 0) { c.setLine(curLine - 1); } return std::abs(view->cursorToCoordinate(c).y() - currentHeight); } static QRect getItemBoundingRect(const KUrl& viewUrl, KTextEditor::View* view, KTextEditor::Cursor itemPosition) { DUChainReadLocker lock; KTextEditor::Range itemRange = DUChainUtils::itemRangeUnderCursor(viewUrl, SimpleCursor(itemPosition)); QPoint startPoint = view->mapToGlobal(view->cursorToCoordinate(itemRange.start())); QPoint endPoint = view->mapToGlobal(view->cursorToCoordinate(itemRange.end())); endPoint.ry() += getLineHeight(view, itemPosition.line()); return QRect(startPoint, endPoint); } 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 KUrl viewUrl(view->document()->url()); QList languages = ICore::self()->languageController()->languagesForUrl(viewUrl); QWidget* navigationWidget = 0; { DUChainReadLocker lock(DUChain::lock()); foreach( ILanguage* language, languages) { navigationWidget = language->languageSupport()->specialLanguageObjectNavigationWidget(viewUrl, SimpleCursor(position)); if(navigationWidget) break; } if(!navigationWidget) { Declaration* decl = DUChainUtils::declarationForDefinition( DUChainUtils::itemUnderCursor(viewUrl, SimpleCursor(position)) ); if (decl && decl->kind() == Declaration::Alias) { AliasDeclaration* alias = dynamic_cast(decl); Q_ASSERT(alias); DUChainReadLocker lock; decl = alias->aliasedDeclaration().declaration(); } if(decl) { if(m_currentToolTipDeclaration == IndexedDeclaration(decl) && m_currentToolTip) return; m_currentToolTipDeclaration = IndexedDeclaration(decl); navigationWidget = decl->context()->createNavigationWidget(decl, DUChainUtils::standardContextForUrl(viewUrl)); } } } 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 = 0; m_currentNavigationWidget = 0; } KDevelop::NavigationToolTip* tooltip = new KDevelop::NavigationToolTip(view, view->mapToGlobal(view->cursorToCoordinate(position)) + QPoint(20, 40), navigationWidget); tooltip->addExtendRect(getItemBoundingRect(viewUrl, view, position)); tooltip->resize( navigationWidget->sizeHint() + QSize(10, 10) ); kDebug() << "tooltip size" << tooltip->size(); m_currentToolTip = tooltip; m_currentNavigationWidget = navigationWidget; ActiveToolTip::showToolTip(tooltip); if ( ! navigationWidget->property("DoNotCloseOnCursorMove").toBool() ) { connect(view, SIGNAL(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)), this, SLOT(hideToolTip()), Qt::UniqueConnection); } else { disconnect(view, SIGNAL(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)), this, SLOT(hideToolTip())); } connect(view, SIGNAL(focusOut(KTextEditor::View*)), this, SLOT(hideToolTip()), Qt::UniqueConnection); }else{ kDebug() << "not showing tooltip, no navigation-widget"; } } void ContextBrowserPlugin::clearMouseHover() { m_mouseHoverCursor = SimpleCursor::invalid(); m_mouseHoverDocument.clear(); } Attribute::Ptr highlightedUseAttribute() { static Attribute::Ptr standardAttribute = Attribute::Ptr(); if( !standardAttribute ) { standardAttribute= Attribute::Ptr( new Attribute() ); standardAttribute->setBackgroundFillWhitespace(true); // mixing (255, 255, 0, 100) with white yields this: standardAttribute->setBackground(QColor(251, 250, 150)); // force a foreground color to overwrite default Kate highlighting, i.e. of Q_OBJECT or similar // foreground color could change, hence apply it everytime standardAttribute->setForeground(QColor(0, 0, 0, 255)); //Don't use alpha here, as kate uses the alpha only to blend with the document background color } return standardAttribute; } Attribute::Ptr highlightedSpecialObjectAttribute() { static Attribute::Ptr standardAttribute = Attribute::Ptr(); if( !standardAttribute ) { standardAttribute = Attribute::Ptr( new Attribute() ); standardAttribute->setBackgroundFillWhitespace(true); // mixing (90, 255, 0, 100) with white yields this: standardAttribute->setBackground(QColor(190, 255, 155)); // force a foreground color to overwrite default Kate highlighting, i.e. of Q_OBJECT or similar // foreground color could change, hence apply it everytime standardAttribute->setForeground(QColor(0, 0, 0, 255)); //Don't use alpha here, as kate uses the alpha only to blend with the document background color } return standardAttribute; } void ContextBrowserPlugin::addHighlight( View* view, KDevelop::Declaration* decl ) { if( !view || !decl ) { kDebug() << "invalid view/declaration"; return; } ViewHighlights& highlights(m_highlightedRanges[view]); KDevelop::DUChainReadLocker lock; // Highlight the declaration highlights.highlights << decl->createRangeMoving(); highlights.highlights.back()->setAttribute(highlightedUseAttribute()); highlights.highlights.back()->setZDepth(highlightingZDepth); // Highlight uses { QMap< IndexedString, QList< SimpleRange > > currentRevisionUses = decl->usesCurrentRevision(); for(QMap< IndexedString, QList< SimpleRange > >::iterator fileIt = currentRevisionUses.begin(); fileIt != currentRevisionUses.end(); ++fileIt) { for(QList< SimpleRange >::const_iterator useIt = (*fileIt).constBegin(); useIt != (*fileIt).constEnd(); ++useIt) { highlights.highlights << PersistentMovingRange::Ptr(new PersistentMovingRange(*useIt, fileIt.key())); highlights.highlights.back()->setAttribute(highlightedUseAttribute()); highlights.highlights.back()->setZDepth(highlightingZDepth); } } } if( FunctionDefinition* def = FunctionDefinition::definition(decl) ) { highlights.highlights << def->createRangeMoving(); highlights.highlights.back()->setAttribute(highlightedUseAttribute()); highlights.highlights.back()->setZDepth(highlightingZDepth); } } Declaration* ContextBrowserPlugin::findDeclaration(View* view, const SimpleCursor& position, bool mouseHighlight) { Q_UNUSED(mouseHighlight); Declaration* foundDeclaration = 0; 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) ); if (foundDeclaration && foundDeclaration->kind() == Declaration::Alias) { AliasDeclaration* 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 0; } 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]; KUrl url = view->document()->url(); IDocument* activeDoc = core()->documentController()->activeDocument(); bool mouseHighlight = (url == m_mouseHoverDocument) && (m_mouseHoverCursor.isValid()); bool shouldUpdateBrowser = (mouseHighlight || (view->isActiveView() && activeDoc && activeDoc->textDocument() == view->document())); SimpleCursor highlightPosition; if (mouseHighlight) highlightPosition = m_mouseHoverCursor; else highlightPosition = SimpleCursor(view->cursorPosition()); ///Pick a language ILanguage* language = 0; if(ICore::self()->languageController()->languagesForUrl(url).isEmpty()) { kDebug() << "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) SimpleRange specialRange = language->languageSupport()->specialLanguageObjectRange(url, highlightPosition); ContextBrowserView* updateBrowserView = shouldUpdateBrowser ? browserViewForWidget(view) : 0; if(specialRange.isValid()) { // Highlight a special language object if(allowHighlight) { highlights.highlights << PersistentMovingRange::Ptr(new PersistentMovingRange(specialRange, IndexedString(url))); highlights.highlights.back()->setAttribute(highlightedSpecialObjectAttribute()); highlights.highlights.back()->setZDepth(highlightingZDepth); } if(updateBrowserView) updateBrowserView->setSpecialNavigationWidget(language->languageSupport()->specialLanguageObjectNavigationWidget(url, highlightPosition)); }else{ KDevelop::DUChainReadLocker lock( DUChain::lock(), 100 ); if(!lock.locked()) { kDebug() << "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 == SimpleCursor(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()); if(core()->documentController()->activeDocument() && core()->documentController()->activeDocument()->textDocument() && core()->documentController()->activeDocument()->textDocument()->activeView()) m_updateViews << core()->documentController()->activeDocument()->textDocument()->activeView(); m_updateTimer->start(highlightingTimeout); // triggers updateViews() } void ContextBrowserPlugin::parseJobFinished(KDevelop::ParseJob* job) { for(QMap< View*, ViewHighlights >::iterator it = m_highlightedRanges.begin(); it != m_highlightedRanges.end(); ++it) { if(it.key()->document()->url() == job->document().toUrl()) { if(m_updateViews.isEmpty()) m_updateTimer->start(highlightingTimeout); if(!m_updateViews.contains(it.key())) { kDebug() << "adding view for update"; m_updateViews << it.key(); // Don't change the highlighted declaration after finished parse-jobs (*it).keep = true; } } } } void ContextBrowserPlugin::textDocumentCreated( KDevelop::IDocument* document ) { Q_ASSERT(document->textDocument()); connect( document->textDocument(), SIGNAL(viewCreated(KTextEditor::Document*,KTextEditor::View*)), this, SLOT(viewCreated(KTextEditor::Document*,KTextEditor::View*)) ); foreach( View* view, document->textDocument()->views() ) viewCreated( document->textDocument(), view ); } void ContextBrowserPlugin::documentActivated( IDocument* doc ) { m_outlineLine->clear(); if (doc->textDocument() && doc->textDocument()->activeView()) { cursorPositionChanged(doc->textDocument()->activeView(), doc->textDocument()->activeView()->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 ) { if(view->document() == m_lastInsertionDocument && newPosition == m_lastInsertionPos) { //Do not update the highlighting while typing m_lastInsertionDocument = 0; m_lastInsertionPos = KTextEditor::Cursor(); if(m_highlightedRanges.contains(view)) m_highlightedRanges[view].keep = true; }else{ if(m_highlightedRanges.contains(view)) m_highlightedRanges[view].keep = false; } clearMouseHover(); m_updateViews.insert(view); m_updateTimer->start(highlightingTimeout/2); // triggers updateViews() } void ContextBrowserPlugin::textInserted(KTextEditor::Document* doc, KTextEditor::Range range) { m_lastInsertionDocument = doc; m_lastInsertionPos = range.end(); } void ContextBrowserPlugin::viewCreated( KTextEditor::Document* , View* v ) { disconnect( v, SIGNAL(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)), this, SLOT(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)) ); ///Just to make sure that multiple connections don't happen connect( v, SIGNAL(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)), this, SLOT(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)) ); connect( v, SIGNAL(destroyed(QObject*)), this, SLOT(viewDestroyed(QObject*)) ); disconnect( v->document(), SIGNAL(textInserted(KTextEditor::Document*,KTextEditor::Range)), this, SLOT(textInserted(KTextEditor::Document*,KTextEditor::Range))); connect( v->document(), SIGNAL(textInserted(KTextEditor::Document*,KTextEditor::Range)), this, SLOT(textInserted(KTextEditor::Document*,KTextEditor::Range))); disconnect( v, SIGNAL(selectionChanged(KTextEditor::View*)), this, SLOT(selectionChanged(KTextEditor::View*))); KTextEditor::TextHintInterface *iface = dynamic_cast(v); if( !iface ) return; iface->enableTextHints(highlightingTimeout); connect(v, SIGNAL(needTextHint(KTextEditor::Cursor,QString&)), this, SLOT(textHintRequested(KTextEditor::Cursor,QString&))); } void ContextBrowserPlugin::registerToolView(ContextBrowserView* view) { m_views << view; } void ContextBrowserPlugin::previousUseShortcut() { switchUse(false); } void ContextBrowserPlugin::nextUseShortcut() { switchUse(true); } KTextEditor::Range cursorToRange(SimpleCursor cursor) { return KTextEditor::Range(cursor.textCursor(), cursor.textCursor()); } void ContextBrowserPlugin::switchUse(bool forward) { if(core()->documentController()->activeDocument() && core()->documentController()->activeDocument()->textDocument() && core()->documentController()->activeDocument()->textDocument()->activeView()) { KTextEditor::Document* doc = core()->documentController()->activeDocument()->textDocument(); KDevelop::DUChainReadLocker lock( DUChain::lock() ); KDevelop::TopDUContext* chosen = DUChainUtils::standardContextForUrl(doc->url()); if( chosen ) { SimpleCursor cCurrent(doc->activeView()->cursorPosition()); KDevelop::CursorInRevision c = chosen->transformToLocalRevision(cCurrent); Declaration* decl = 0; //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); if (decl && decl->kind() == Declaration::Alias) { AliasDeclaration* alias = dynamic_cast(decl); Q_ASSERT(alias); DUChainReadLocker lock; decl = alias->aliasedDeclaration().declaration(); } if(decl) { Declaration* target = 0; 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) { SimpleCursor jumpTo = target->rangeInCurrentRevision().start; KUrl 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) { QList useRanges = allUses(top, decl, true); qSort(useRanges); if(!useRanges.isEmpty()) { KUrl url = top->url().toUrl(); SimpleRange 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 QList localUses = allUses(chosen, decl, true); qSort(localUses); 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 kDebug() << "count of uses:" << localUses.size() << "nextUse" << nextUse; if(nextUse < 0 || nextUse == localUses.size()) { kDebug() << "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); kDebug() << "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; } KUrl u(decl->url().str()); SimpleRange range = decl->rangeInCurrentRevision(); range.end = range.start; lock.unlock(); core()->documentController()->openDocument(u, range.textRange()); return; }else{ TopDUContext* nextTop = usingFiles[nextFile].data(); KUrl u(nextTop->url().str()); QList nextTopUses = allUses(nextTop, decl, true); qSort(nextTopUses); if(!nextTopUses.isEmpty()) { SimpleRange range = chosen->transformFromLocalRevision(forward ? nextTopUses.front() : nextTopUses.back()); range.end = range.start; lock.unlock(); core()->documentController()->openDocument(u, range.textRange()); } return; } }else{ kDebug() << "not found own file in use list"; } }else{ KUrl url(chosen->url().str()); SimpleRange range = chosen->transformFromLocalRevision(localUses[nextUse]); range.end = range.start; lock.unlock(); core()->documentController()->openDocument(url, range.textRange()); 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()) { kDebug() << "updating jump source"; DUContext* context = getContextAt(previousDocument->url(), previousCursor); if(context) { updateHistory(context, SimpleCursor(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()), SimpleCursor(previousCursor)))); ++m_nextHistoryIndex; } } kDebug() << "new doc: " << newDocument << " new cursor: " << newCursor; if(newDocument && newCursor.isValid()) { kDebug() << "updating jump target"; DUContext* context = getContextAt(newDocument->url(), newCursor); if(context) { updateHistory(context, SimpleCursor(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()), SimpleCursor(newCursor)))); ++m_nextHistoryIndex; 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(), SIGNAL(documentJumpPerformed(KDevelop::IDocument*,KTextEditor::Cursor,KDevelop::IDocument*,KTextEditor::Cursor)), this, SLOT(documentJumpPerformed(KDevelop::IDocument*,KTextEditor::Cursor,KDevelop::IDocument*,KTextEditor::Cursor))); ICore::self()->documentController()->openDocument(c.document.toUrl(), c.textCursor()); connect(ICore::self()->documentController(), SIGNAL(documentJumpPerformed(KDevelop::IDocument*,KTextEditor::Cursor,KDevelop::IDocument*,KTextEditor::Cursor)), this, SLOT(documentJumpPerformed(KDevelop::IDocument*,KTextEditor::Cursor,KDevelop::IDocument*,KTextEditor::Cursor))); 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 = ""; actionText += " @ "; QString fileName = entry.absoluteCursorPosition.document.toUrl().fileName(); actionText += QString("%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; for(int a = m_nextHistoryIndex; a < m_history.size(); ++a) { indices << a; } fillHistoryPopup(m_nextMenu, indices); } void ContextBrowserPlugin::previousMenuAboutToShow() { QList indices; 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() ); foreach(int index, historyIndices) { QAction* action = new QAction(actionTextFor(index), menu); action->setData(index); menu->addAction(action); connect(action, SIGNAL(triggered(bool)), this, SLOT(actionTriggered())); } } bool ContextBrowserPlugin::isPreviousEntry(KDevelop::DUContext* context, const KDevelop::SimpleCursor& /*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 KDevelop::SimpleCursor& position, bool force) { kDebug() << "updating history"; if(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 = m_history.mid(m_history.size() - maxHistoryLength); m_nextHistoryIndex = m_history.size(); } } } void ContextBrowserPlugin::setAllowBrowsing(bool allow) { m_browseButton->setChecked(allow); } void ContextBrowserPlugin::updateDeclarationListBox(DUContext* context) { if(!context || !context->owner()) { kDebug() << "not updating box"; m_listUrl = IndexedString(); ///@todo Compute the context in the document here 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->hasFocus()) { m_outlineLine->setText(text); m_outlineLine->setCursorPosition(0); } kDebug() << "updated" << text; } void ContextBrowserPlugin::actionTriggered() { QAction* action = qobject_cast(sender()); Q_ASSERT(action); Q_ASSERT(action->data().type() == QVariant::Int); int historyPosition = action->data().toInt(); // kDebug() << "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) { KTextEditor::View* view = qobject_cast(sender()); if(!view) { kWarning() << "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) { AbstractNavigationWidget* 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; } } } } 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(KDevelop::DocumentCursor pos) : absoluteCursorPosition(pos) { } ContextBrowserPlugin::HistoryEntry::HistoryEntry(IndexedDUContext ctx, const KDevelop::SimpleCursor& 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.line += context.data()->range().start.line; }else{ ret = absoluteCursorPosition; } return ret; } void ContextBrowserPlugin::HistoryEntry::setCursorPosition(const KDevelop::SimpleCursor& cursorPosition) { KDevelop::DUChainReadLocker lock( KDevelop::DUChain::lock() ); if(context.data()) { absoluteCursorPosition = DocumentCursor(context.data()->url(), cursorPosition); relativeCursorPosition = cursorPosition; relativeCursorPosition.line -= context.data()->range().start.line; } } // kate: space-indent on; indent-width 2; tab-width 4; replace-tabs on; auto-insert-doxygen on