/* * This file is part of KDevelop * * Copyright 2010 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 "documentchangetracker.h" #include #include #include #include #include #include #include #include #include #include #include #include "backgroundparser.h" #include // 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(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(); } }