kde-extraapps/kdevplatform/language/backgroundparser/documentchangetracker.cpp
2015-07-26 14:23:17 +03:00

562 lines
19 KiB
C++

/*
* This file is part of KDevelop
*
* Copyright 2010 David Nolden <david.nolden.kdevelop@art-master.de>
*
* 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 "documentchangetracker.h"
#include <QMutex>
#include <QMutexLocker>
#include <kdebug.h>
#include <ktexteditor/document.h>
#include <ktexteditor/movinginterface.h>
#include <interfaces/foregroundlock.h>
#include <editor/modificationrevisionset.h>
#include <duchain/indexedstring.h>
#include <interfaces/icore.h>
#include <interfaces/ilanguagecontroller.h>
#include <interfaces/ilanguage.h>
#include "backgroundparser.h"
#include <QApplication>
// Can be used to disable the 'clever' updating logic that ignores whitespace-only changes and such.
// #define ALWAYS_UPDATE
using namespace KTextEditor;
/**
* @todo Track the exact changes to the document, and then:
* Dont reparse if:
* - Comment added/changed
* - Newlines added/changed (ready)
* Complete the document for validation:
* - Incomplete for-loops
* - ...
* Only reparse after a statement was completed (either with statement-completion or manually), or after the cursor was switched away
* Incremental parsing:
* - All changes within a local function (or function parameter context): Update only the context (and all its importers)
*
* @todo: Prevent recursive updates after insignificant changes
* (whitespace changes, or changes that don't affect publically visible stuff, eg. local incremental changes)
* -> Maybe alter the file-modification caches directly
* */
namespace {
QRegExp whiteSpaceRegExp("\\s");
};
namespace KDevelop
{
DocumentChangeTracker::DocumentChangeTracker( KTextEditor::Document* document )
: m_needUpdate(false), m_changedRange(0), m_document(document), m_moving(0), m_whitespaceSensitivity(ILanguageSupport::Insensitive)
{
m_url = IndexedString(document->url());
Q_ASSERT(document);
Q_ASSERT(document->url().isValid());
// Check whether a language plugin is tracking the document which belongs to a
// whitespace-sensitive language (e.g. python)
foreach ( KDevelop::ILanguage* lang, ICore::self()->languageController()->languagesForUrl(document->url()) ) {
if ( ! lang || ! lang->languageSupport() ) {
continue;
}
if ( lang->languageSupport()->whitespaceSensititivy() >= m_whitespaceSensitivity ) {
m_whitespaceSensitivity = lang->languageSupport()->whitespaceSensititivy();
}
}
connect(document, SIGNAL(textInserted(KTextEditor::Document*,KTextEditor::Range)), SLOT(textInserted(KTextEditor::Document*,KTextEditor::Range)));
connect(document, SIGNAL(textRemoved(KTextEditor::Document*,KTextEditor::Range,QString)), SLOT(textRemoved(KTextEditor::Document*,KTextEditor::Range,QString)));
connect(document, SIGNAL(textChanged(KTextEditor::Document*,KTextEditor::Range,QString,KTextEditor::Range)), SLOT(textChanged(KTextEditor::Document*,KTextEditor::Range,QString,KTextEditor::Range)));
connect(document, SIGNAL(destroyed(QObject*)), SLOT(documentDestroyed(QObject*)));
connect(document, SIGNAL(documentSavedOrUploaded(KTextEditor::Document*,bool)), SLOT(documentSavedOrUploaded(KTextEditor::Document*,bool)));
m_moving = dynamic_cast<KTextEditor::MovingInterface*>(document);
Q_ASSERT(m_moving);
m_changedRange = m_moving->newMovingRange(KTextEditor::Range(), KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight);
connect(m_document, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)));
ModificationRevision::setEditorRevisionForFile(m_url, m_moving->revision());
reset();
}
QList< QPair< SimpleRange, QString > > DocumentChangeTracker::completions() const
{
VERIFY_FOREGROUND_LOCKED
QList< QPair< SimpleRange , QString > > ret;
return ret;
}
Range DocumentChangeTracker::changedRange() const
{
VERIFY_FOREGROUND_LOCKED
return m_changedRange->toRange();
}
void DocumentChangeTracker::reset()
{
VERIFY_FOREGROUND_LOCKED
// We don't reset the insertion here, as it may continue
m_needUpdate = false;
m_changedRange->setRange(KTextEditor::Range::invalid());
m_revisionAtLastReset = acquireRevision(m_moving->revision());
Q_ASSERT(m_revisionAtLastReset);
m_textAtLastReset = m_document->text();
}
RevisionReference DocumentChangeTracker::currentRevision()
{
VERIFY_FOREGROUND_LOCKED
return acquireRevision(m_moving->revision());
}
RevisionReference DocumentChangeTracker::revisionAtLastReset() const
{
VERIFY_FOREGROUND_LOCKED
return m_revisionAtLastReset;
}
QString DocumentChangeTracker::textAtLastReset() const
{
VERIFY_FOREGROUND_LOCKED
return m_textAtLastReset;
}
bool DocumentChangeTracker::needUpdate() const
{
VERIFY_FOREGROUND_LOCKED
return m_needUpdate;
}
bool DocumentChangeTracker::checkMergeTokens(const KTextEditor::Range& range, QString /*oldText*/, QString /*newText*/)
{
///@todo Improve this so that it notices when we wrapped in/out of a line-comment
///@todo Improve this so that it really checks whether some merge-able tokens have been moved together
if(m_document->documentRange().contains(range))
{
if(range.start().column() == 0 || m_document->text(KTextEditor::Range(range.start().line(), range.start().column()-1, range.start().line(), range.start().column()))[0].isSpace())
return true;
if(range.end().column() >= m_document->lineLength(range.end().line()) || m_document->text(KTextEditor::Range(range.end().line(), range.end().column(), range.end().line(), range.end().column()+1))[0].isSpace())
return true;
}
return false;
}
void DocumentChangeTracker::textChanged( Document* document, Range /*oldRange*/, QString oldText, Range newRange )
{
m_currentCleanedInsertion.clear();
QString newText = document->text(newRange);
QString oldTextWithoutWhitespace = oldText;
oldTextWithoutWhitespace.remove(whiteSpaceRegExp);
QString newTextWithoutWhitespace = newText;
newTextWithoutWhitespace.remove(whiteSpaceRegExp);
if(oldTextWithoutWhitespace.isEmpty() && newTextWithoutWhitespace.isEmpty() && checkMergeTokens(newRange, oldText, newText))
{
// Only whitespace was changed, no update is required
}else{
m_needUpdate = true;
}
#ifdef ALWAYS_UPDATE
m_needUpdate = true;
#endif
m_currentCleanedInsertion.clear();
m_lastInsertionPosition = KTextEditor::Cursor::invalid();
updateChangedRange(newRange);
}
void DocumentChangeTracker::updateChangedRange( Range changed )
{
if(m_changedRange->toRange() == KTextEditor::Range::invalid())
m_changedRange->setRange(changed);
else
m_changedRange->setRange(changed.encompass(m_changedRange->toRange()));
// Q_ASSERT(m_moving->revision() != m_revisionAtLastReset->revision()); // May happen after reload
// When reloading, textRemoved is called with an invalid m_document->url(). For that reason, we use m_url instead.
ModificationRevision::setEditorRevisionForFile(m_url, m_moving->revision());
if(needUpdate())
ICore::self()->languageController()->backgroundParser()->addDocument(m_url, TopDUContext::AllDeclarationsContextsAndUses);
}
void DocumentChangeTracker::textInserted( Document* document, Range range )
{
QString text = document->text(range);
if ( m_whitespaceSensitivity == ILanguageSupport::Sensitive ) {
m_needUpdate = true;
}
if ( ! m_needUpdate ) {
QString textWithoutWhitespace = text;
textWithoutWhitespace.remove(whiteSpaceRegExp);
bool changeIsWhitespaceOnly = textWithoutWhitespace.isEmpty() && checkMergeTokens(range, "", text);
if ( m_whitespaceSensitivity == ILanguageSupport::IndentOnly && changeIsWhitespaceOnly ) {
// The language requires a document to be re-parsed if the indentation changes (e.g. python),
// so do some special checks here to see if that is the case.
if ( changeIsWhitespaceOnly ) {
QString fromLineBeginning = document->text(Range(Cursor(range.start().line(), 0), range.end()));
bool inIndent = true;
for ( int i = fromLineBeginning.length() - 1; i >= 0; --i ) {
if ( ! fromLineBeginning.at(i).isSpace() ) {
inIndent = false;
break;
}
}
if ( inIndent ) {
m_needUpdate = true;
}
}
}
if ( ! changeIsWhitespaceOnly ) {
// If we've inserted something else than whitespace, an update is required for all languages.
m_needUpdate = true;
}
}
#ifdef ALWAYS_UPDATE
m_needUpdate = true;
#endif
if(m_lastInsertionPosition == KTextEditor::Cursor::invalid() || m_lastInsertionPosition == range.start())
{
m_currentCleanedInsertion.append(text);
m_lastInsertionPosition = range.end();
}
updateChangedRange(range);
}
void DocumentChangeTracker::textRemoved( Document* /*document*/, Range oldRange, QString oldText )
{
QString text = oldText;
QString textWithoutWhitespace = text;
textWithoutWhitespace.remove(whiteSpaceRegExp);
if(textWithoutWhitespace.isEmpty() && checkMergeTokens(Range(oldRange.start(), oldRange.start()), oldText, ""))
{
// Only whitespace was changed, no update is required
}else{
m_needUpdate = true; // If we've inserted something else than whitespace, an update is required
}
#ifdef ALWAYS_UPDATE
m_needUpdate = true;
#endif
m_currentCleanedInsertion.clear();
m_lastInsertionPosition = KTextEditor::Cursor::invalid();
updateChangedRange(oldRange);
}
void DocumentChangeTracker::documentSavedOrUploaded(KTextEditor::Document* doc,bool)
{
ModificationRevision::clearModificationCache(IndexedString(doc->url()));
}
void DocumentChangeTracker::documentDestroyed( QObject* )
{
m_document = 0;
m_moving = 0;
m_changedRange = 0;
}
DocumentChangeTracker::~DocumentChangeTracker()
{
Q_ASSERT(m_document);
ModificationRevision::clearEditorRevisionForFile(KDevelop::IndexedString(m_document->url()));
}
Document* DocumentChangeTracker::document() const
{
return m_document;
}
MovingInterface* DocumentChangeTracker::documentMovingInterface() const
{
return m_moving;
}
void DocumentChangeTracker::aboutToInvalidateMovingInterfaceContent ( Document* )
{
// Release all revisions! They must not be used any more.
kDebug() << "clearing all revisions";
m_revisionLocks.clear();
m_revisionAtLastReset = RevisionReference();
ModificationRevision::setEditorRevisionForFile(m_url, 0);
}
KDevelop::RangeInRevision DocumentChangeTracker::transformBetweenRevisions(KDevelop::RangeInRevision range, qint64 fromRevision, qint64 toRevision) const
{
VERIFY_FOREGROUND_LOCKED
if((fromRevision == -1 || holdingRevision(fromRevision)) && (toRevision == -1 || holdingRevision(toRevision)))
{
m_moving->transformCursor(range.start.line, range.start.column, KTextEditor::MovingCursor::MoveOnInsert, fromRevision, toRevision);
m_moving->transformCursor(range.end.line, range.end.column, KTextEditor::MovingCursor::StayOnInsert, fromRevision, toRevision);
}
return range;
}
KDevelop::CursorInRevision DocumentChangeTracker::transformBetweenRevisions(KDevelop::CursorInRevision cursor, qint64 fromRevision, qint64 toRevision, KTextEditor::MovingCursor::InsertBehavior behavior) const
{
VERIFY_FOREGROUND_LOCKED
if((fromRevision == -1 || holdingRevision(fromRevision)) && (toRevision == -1 || holdingRevision(toRevision)))
{
m_moving->transformCursor(cursor.line, cursor.column, behavior, fromRevision, toRevision);
}
return cursor;
}
RangeInRevision DocumentChangeTracker::transformToRevision(SimpleRange range, qint64 toRevision) const
{
return transformBetweenRevisions(RangeInRevision::castFromSimpleRange(range), -1, toRevision);
}
CursorInRevision DocumentChangeTracker::transformToRevision(SimpleCursor cursor, qint64 toRevision, MovingCursor::InsertBehavior behavior) const
{
return transformBetweenRevisions(CursorInRevision::castFromSimpleCursor(cursor), -1, toRevision, behavior);
}
SimpleRange DocumentChangeTracker::transformToCurrentRevision(RangeInRevision range, qint64 fromRevision) const
{
return transformBetweenRevisions(range, fromRevision, -1).castToSimpleRange();
}
SimpleCursor DocumentChangeTracker::transformToCurrentRevision(CursorInRevision cursor, qint64 fromRevision, MovingCursor::InsertBehavior behavior) const
{
return transformBetweenRevisions(cursor, fromRevision, -1, behavior).castToSimpleCursor();
}
RevisionLockerAndClearerPrivate::RevisionLockerAndClearerPrivate(DocumentChangeTracker* tracker, qint64 revision) : m_tracker(tracker), m_revision(revision)
{
VERIFY_FOREGROUND_LOCKED
moveToThread(QApplication::instance()->thread());
// Lock the revision
m_tracker->lockRevision(revision);
}
RevisionLockerAndClearerPrivate::~RevisionLockerAndClearerPrivate() {
if (m_tracker)
m_tracker->unlockRevision(m_revision);
}
RevisionLockerAndClearer::~RevisionLockerAndClearer()
{
m_p->deleteLater(); // Will be deleted in the foreground thread, as the object was re-owned to the foreground
}
RevisionReference DocumentChangeTracker::acquireRevision(qint64 revision)
{
VERIFY_FOREGROUND_LOCKED
if(!holdingRevision(revision) && revision != m_moving->revision())
return RevisionReference();
RevisionReference ret(new RevisionLockerAndClearer);
ret->m_p = new RevisionLockerAndClearerPrivate(this, revision);
return ret;
}
bool DocumentChangeTracker::holdingRevision(qint64 revision) const
{
VERIFY_FOREGROUND_LOCKED
return m_revisionLocks.contains(revision);
}
void DocumentChangeTracker::lockRevision(qint64 revision)
{
VERIFY_FOREGROUND_LOCKED
QMap< qint64, int >::iterator it = m_revisionLocks.find(revision);
if(it != m_revisionLocks.end())
++(*it);
else
{
m_revisionLocks.insert(revision, 1);
m_moving->lockRevision(revision);
}
}
void DocumentChangeTracker::unlockRevision(qint64 revision)
{
VERIFY_FOREGROUND_LOCKED
QMap< qint64, int >::iterator it = m_revisionLocks.find(revision);
if(it == m_revisionLocks.end())
{
kDebug() << "cannot unlock revision" << revision << ", probably the revisions have been cleared";
return;
}
--(*it);
if(*it == 0)
{
m_moving->unlockRevision(revision);
m_revisionLocks.erase(it);
}
}
qint64 RevisionLockerAndClearer::revision() const
{
return m_p->revision();
}
RangeInRevision RevisionLockerAndClearer::transformToRevision(const KDevelop::RangeInRevision& range,
const KDevelop::RevisionLockerAndClearer::Ptr& to) const
{
VERIFY_FOREGROUND_LOCKED
if(!m_p->m_tracker || !valid() || (to && !to->valid()))
return range;
qint64 fromRevision = revision();
qint64 toRevision = -1;
if(to)
toRevision = to->revision();
return m_p->m_tracker->transformBetweenRevisions(range, fromRevision, toRevision);
}
CursorInRevision RevisionLockerAndClearer::transformToRevision(const KDevelop::CursorInRevision& cursor,
const KDevelop::RevisionLockerAndClearer::Ptr& to,
MovingCursor::InsertBehavior behavior) const
{
VERIFY_FOREGROUND_LOCKED
if(!m_p->m_tracker || !valid() || (to && !to->valid()))
return cursor;
qint64 fromRevision = revision();
qint64 toRevision = -1;
if(to)
toRevision = to->revision();
return m_p->m_tracker->transformBetweenRevisions(cursor, fromRevision, toRevision, behavior);
}
RangeInRevision RevisionLockerAndClearer::transformFromRevision(const KDevelop::RangeInRevision& range,
const KDevelop::RevisionLockerAndClearer::Ptr& from) const
{
VERIFY_FOREGROUND_LOCKED
if(!m_p->m_tracker || !valid())
return range;
qint64 toRevision = revision();
qint64 fromRevision = -1;
if(from)
fromRevision = from->revision();
return m_p->m_tracker->transformBetweenRevisions(range, fromRevision, toRevision);
}
CursorInRevision RevisionLockerAndClearer::transformFromRevision(const KDevelop::CursorInRevision& cursor,
const KDevelop::RevisionLockerAndClearer::Ptr& from,
MovingCursor::InsertBehavior behavior) const
{
VERIFY_FOREGROUND_LOCKED
if(!m_p->m_tracker)
return cursor;
qint64 toRevision = revision();
qint64 fromRevision = -1;
if(from)
fromRevision = from->revision();
return m_p->m_tracker->transformBetweenRevisions(cursor, fromRevision, toRevision, behavior);
}
SimpleRange RevisionLockerAndClearer::transformToCurrentRevision(const KDevelop::RangeInRevision& range) const
{
return transformToRevision(range, KDevelop::RevisionLockerAndClearer::Ptr()).castToSimpleRange();
}
SimpleCursor RevisionLockerAndClearer::transformToCurrentRevision(const KDevelop::CursorInRevision& cursor,
MovingCursor::InsertBehavior behavior) const
{
return transformToRevision(cursor, KDevelop::RevisionLockerAndClearer::Ptr(), behavior).castToSimpleCursor();
}
RangeInRevision RevisionLockerAndClearer::transformFromCurrentRevision(const KDevelop::SimpleRange& range) const
{
return transformFromRevision(RangeInRevision::castFromSimpleRange(range), RevisionReference());
}
CursorInRevision RevisionLockerAndClearer::transformFromCurrentRevision(const KDevelop::SimpleCursor& cursor,
MovingCursor::InsertBehavior behavior) const
{
return transformFromRevision(CursorInRevision::castFromSimpleCursor(cursor), RevisionReference(), behavior);
}
bool RevisionLockerAndClearer::valid() const
{
VERIFY_FOREGROUND_LOCKED
if(!m_p->m_tracker)
return false;
if(revision() == -1)
return true; // The 'current' revision is always valid
return m_p->m_tracker->holdingRevision(revision());
}
RevisionReference DocumentChangeTracker::diskRevision() const
{
///@todo Track which revision was last saved to disk
return RevisionReference();
}
}