mirror of
https://bitbucket.org/smil3y/kde-extraapps.git
synced 2025-02-26 20:03:10 +00:00
562 lines
19 KiB
C++
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();
|
|
}
|
|
|
|
}
|