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

574 lines
23 KiB
C++

/*
Copyright 2008 David Nolden <david.nolden.kdevelop@art-master.de>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License version 2 as published by the Free Software Foundation.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "documentchangeset.h"
#include "coderepresentation.h"
#include <algorithm>
#include <QStringList>
#include <KLocalizedString>
#include <interfaces/icore.h>
#include <interfaces/ilanguagecontroller.h>
#include <interfaces/idocumentcontroller.h>
#include <language/duchain/duchain.h>
#include <language/duchain/duchainlock.h>
#include <language/duchain/duchainutils.h>
#include <language/duchain/parsingenvironment.h>
#include <language/backgroundparser/backgroundparser.h>
#include <language/editor/modificationrevisionset.h>
#include <interfaces/isourceformattercontroller.h>
#include <interfaces/isourceformatter.h>
#include <interfaces/iproject.h>
#include <interfaces/iprojectcontroller.h>
#include <project/projectmodel.h>
namespace KDevelop {
typedef QList<DocumentChangePointer> ChangesList;
typedef QHash<IndexedString, ChangesList> ChangesHash;
struct DocumentChangeSetPrivate
{
DocumentChangeSet::ReplacementPolicy replacePolicy;
DocumentChangeSet::FormatPolicy formatPolicy;
DocumentChangeSet::DUChainUpdateHandling updatePolicy;
DocumentChangeSet::ActivationPolicy activationPolicy;
ChangesHash changes;
QHash<IndexedString, IndexedString> documentsRename;
DocumentChangeSet::ChangeResult addChange(const DocumentChangePointer& change);
DocumentChangeSet::ChangeResult replaceOldText(CodeRepresentation* repr, const QString& newText,
const ChangesList& sortedChangesList);
DocumentChangeSet::ChangeResult generateNewText(const IndexedString& file,
ChangesList& sortedChanges,
const CodeRepresentation* repr,
QString& output);
DocumentChangeSet::ChangeResult removeDuplicates(const IndexedString& file,
ChangesList& filteredChanges);
void formatChanges();
void updateFiles();
};
// Simple helpers to clear up code clutter
namespace
{
inline bool changeIsValid(const DocumentChange& change, const QStringList& textLines)
{
return change.m_range.start <= change.m_range.end &&
change.m_range.end.line < textLines.size() &&
change.m_range.start.line >= 0 &&
change.m_range.start.column >= 0 &&
change.m_range.start.column <= textLines[change.m_range.start.line].length() &&
change.m_range.end.column >= 0 &&
change.m_range.end.column <= textLines[change.m_range.end.line].length();
}
inline bool duplicateChanges(const DocumentChangePointer& previous, const DocumentChangePointer& current)
{
// Given the option of considering a duplicate two changes in the same range
// but with different old texts to be ignored
return previous->m_range == current->m_range &&
previous->m_newText == current->m_newText &&
(previous->m_oldText == current->m_oldText ||
(previous->m_ignoreOldText && current->m_ignoreOldText));
}
inline QString rangeText(const SimpleRange& range, const QStringList& textLines)
{
QStringList ret;
ret.reserve(range.end.line - range.start.line + 1);
for(int line = range.start.line; line <= range.end.line; ++line) {
const QString lineText = textLines.at(line);
int startColumn = 0;
int endColumn = lineText.length();
if (line == range.start.line) {
startColumn = range.start.column;
}
if (line == range.end.line) {
endColumn = range.end.column;
}
ret << lineText.mid(startColumn, endColumn - startColumn);
}
return ret.join("\n");
}
}
DocumentChangeSet::DocumentChangeSet()
: d(new DocumentChangeSetPrivate)
{
d->replacePolicy = StopOnFailedChange;
d->formatPolicy = AutoFormatChanges;
d->updatePolicy = SimpleUpdate;
d->activationPolicy = DoNotActivate;
}
DocumentChangeSet::DocumentChangeSet(const DocumentChangeSet& rhs)
: d(new DocumentChangeSetPrivate(*rhs.d))
{
}
DocumentChangeSet& DocumentChangeSet::operator=(const DocumentChangeSet& rhs)
{
*d = *rhs.d;
return *this;
}
DocumentChangeSet::~DocumentChangeSet()
{
delete d;
}
DocumentChangeSet::ChangeResult DocumentChangeSet::addChange(const DocumentChange& change)
{
return d->addChange(DocumentChangePointer(new DocumentChange(change)));
}
DocumentChangeSet::ChangeResult DocumentChangeSet::addChange(const DocumentChangePointer& change)
{
return d->addChange(change);
}
DocumentChangeSet::ChangeResult DocumentChangeSet::addDocumentRenameChange(const IndexedString& oldFile,
const IndexedString& newname)
{
d->documentsRename.insert(oldFile, newname);
return true;
}
DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::addChange(const DocumentChangePointer& change)
{
changes[change->m_document].append(change);
return true;
}
void DocumentChangeSet::setReplacementPolicy(DocumentChangeSet::ReplacementPolicy policy)
{
d->replacePolicy = policy;
}
void DocumentChangeSet::setFormatPolicy(DocumentChangeSet::FormatPolicy policy)
{
d->formatPolicy = policy;
}
void DocumentChangeSet::setUpdateHandling(DocumentChangeSet::DUChainUpdateHandling policy)
{
d->updatePolicy = policy;
}
void DocumentChangeSet::setActivationPolicy(DocumentChangeSet::ActivationPolicy policy)
{
d->activationPolicy = policy;
}
DocumentChangeSet::ChangeResult DocumentChangeSet::applyAllChanges()
{
KUrl oldActiveDoc;
if (IDocument* activeDoc = ICore::self()->documentController()->activeDocument()) {
oldActiveDoc = activeDoc->url();
}
// rename files
QHash<IndexedString, IndexedString>::const_iterator it = d->documentsRename.constBegin();
for(; it != d->documentsRename.constEnd(); ++it) {
KUrl url = it.key().toUrl();
IProject* p = ICore::self()->projectController()->findProjectForUrl(url);
if(p) {
QList<ProjectFileItem*> files = p->filesForPath(it.key());
if(!files.isEmpty()) {
ProjectBaseItem::RenameStatus renamed = files.first()->rename(it.value().str());
if(renamed == ProjectBaseItem::RenameOk) {
const KUrl newUrl(url.upUrl(), it.value().str());
if (url == oldActiveDoc) {
oldActiveDoc = newUrl;
}
IndexedString idxNewDoc(newUrl);
// ensure changes operate on new file name
ChangesHash::iterator iter = d->changes.find(it.key());
if (iter != d->changes.end()) {
// copy changes
ChangesList value = iter.value();
// remove old entry
d->changes.erase(iter);
// adapt to new url
ChangesList::iterator itChange = value.begin();
ChangesList::iterator itEnd = value.end();
for(; itChange != itEnd; ++itChange) {
(*itChange)->m_document = idxNewDoc;
}
d->changes[idxNewDoc] = value;
}
} else {
///FIXME: share code with project manager for the error code string representation
return ChangeResult(i18n("Could not rename '%1' to '%2'", url.pathOrUrl(), it.value().str()));
}
} else {
//TODO: do it outside the project management?
kWarning() << "tried to rename file not tracked by project - not implemented";
}
} else {
kWarning() << "tried to rename a file outside of a project - not implemented";
}
}
QMap<IndexedString, CodeRepresentation::Ptr> codeRepresentations;
QMap<IndexedString, QString> newTexts;
ChangesHash filteredSortedChanges;
ChangeResult result(true);
QList<IndexedString> files(d->changes.keys());
foreach(const IndexedString &file, files) {
CodeRepresentation::Ptr repr = createCodeRepresentation(file);
if(!repr) {
return ChangeResult(QString("Could not create a Representation for %1").arg(file.str()));
}
codeRepresentations[file] = repr;
QList<DocumentChangePointer>& sortedChangesList(filteredSortedChanges[file]);
{
result = d->removeDuplicates(file, sortedChangesList);
if(!result)
return result;
}
{
result = d->generateNewText(file, sortedChangesList, repr.data(), newTexts[file]);
if(!result)
return result;
}
}
QMap<IndexedString, QString> oldTexts;
//Apply the changes to the files
foreach(const IndexedString &file, files) {
oldTexts[file] = codeRepresentations[file]->text();
result = d->replaceOldText(codeRepresentations[file].data(), newTexts[file], filteredSortedChanges[file]);
if(!result && d->replacePolicy == StopOnFailedChange) {
//Revert all files
foreach(const IndexedString &revertFile, oldTexts.keys()) {
codeRepresentations[revertFile]->setText(oldTexts[revertFile]);
}
return result;
}
}
d->updateFiles();
if(d->activationPolicy == Activate) {
foreach(const IndexedString& file, files) {
ICore::self()->documentController()->openDocument(file.toUrl());
}
}
// ensure the old document is still activated
if (oldActiveDoc.isValid()) {
ICore::self()->documentController()->openDocument(oldActiveDoc);
}
return result;
}
DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::replaceOldText(CodeRepresentation* repr,
const QString& newText,
const ChangesList& sortedChangesList)
{
DynamicCodeRepresentation* dynamic = dynamic_cast<DynamicCodeRepresentation*>(repr);
if(dynamic) {
dynamic->startEdit();
//Replay the changes one by one
for(int pos = sortedChangesList.size()-1; pos >= 0; --pos) {
const DocumentChange& change(*sortedChangesList[pos]);
if(!dynamic->replace(change.m_range.textRange(), change.m_oldText, change.m_newText, change.m_ignoreOldText))
{
QString warningString = QString("Inconsistent change in %1 at %2:%3 -> %4:%5 = %6(encountered \"%7\") -> \"%8\"")
.arg(change.m_document.str())
.arg(change.m_range.start.line)
.arg(change.m_range.start.column)
.arg(change.m_range.end.line)
.arg(change.m_range.end.column)
.arg(change.m_oldText)
.arg(dynamic->rangeText(change.m_range.textRange()))
.arg(change.m_newText);
if(replacePolicy == DocumentChangeSet::WarnOnFailedChange) {
kWarning() << warningString;
} else if(replacePolicy == DocumentChangeSet::StopOnFailedChange) {
dynamic->endEdit();
return DocumentChangeSet::ChangeResult(warningString);
}
//If set to ignore failed changes just continue with the others
}
}
dynamic->endEdit();
return true;
}
//For files on disk
if (!repr->setText(newText)) {
QString warningString = QString("Could not replace text in the document: %1")
.arg(sortedChangesList.begin()->data()->m_document.str());
if(replacePolicy == DocumentChangeSet::WarnOnFailedChange) {
kWarning() << warningString;
}
return DocumentChangeSet::ChangeResult(warningString);
}
return true;
}
DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::generateNewText(const IndexedString & file,
ChangesList& sortedChanges,
const CodeRepresentation * repr,
QString & output)
{
ISourceFormatter* formatter = 0;
if(ICore::self()) {
formatter = ICore::self()->sourceFormatterController()->formatterForUrl(file.toUrl());
}
//Create the actual new modified file
QStringList textLines = repr->text().split('\n');
KUrl url = file.toUrl();
KMimeType::Ptr mime = KMimeType::findByUrl(url);
QVector<int> removedLines;
for(int pos = sortedChanges.size()-1; pos >= 0; --pos) {
DocumentChange& change(*sortedChanges[pos]);
QString encountered;
if(changeIsValid(change, textLines) && //We demand this, although it should be fixed
((encountered = rangeText(change.m_range, textLines)) == change.m_oldText || change.m_ignoreOldText))
{
///Problem: This does not work if the other changes significantly alter the context @todo Use the changed context
QString leftContext = QStringList(textLines.mid(0, change.m_range.start.line+1)).join("\n");
leftContext.chop(textLines[change.m_range.start.line].length() - change.m_range.start.column);
QString rightContext = QStringList(textLines.mid(change.m_range.end.line)).join("\n").mid(change.m_range.end.column);
if(formatter && (formatPolicy == DocumentChangeSet::AutoFormatChanges
|| formatPolicy == DocumentChangeSet::AutoFormatChangesKeepIndentation))
{
QString oldNewText = change.m_newText;
change.m_newText = formatter->formatSource(change.m_newText, url, mime, leftContext, rightContext);
if(formatPolicy == DocumentChangeSet::AutoFormatChangesKeepIndentation) {
// Reproduce the previous indentation
QStringList oldLines = oldNewText.split('\n');
QStringList newLines = change.m_newText.split('\n');
if(oldLines.size() == newLines.size()) {
for(int line = 0; line < newLines.size(); ++line) {
// Keep the previous indentation
QString oldIndentation;
for (int a = 0; a < oldLines[line].size(); ++a) {
if (oldLines[line][a].isSpace()) {
oldIndentation.append(oldLines[line][a]);
} else {
break;
}
}
int newIndentationLength = 0;
for(int a = 0; a < newLines[line].size(); ++a) {
if(newLines[line][a].isSpace()) {
newIndentationLength = a;
} else {
break;
}
}
newLines[line].replace(0, newIndentationLength, oldIndentation);
}
change.m_newText = newLines.join("\n");
} else {
kDebug() << "Cannot keep the indentation because the line count has changed" << oldNewText;
}
}
}
QString& line = textLines[change.m_range.start.line];
if (change.m_range.start.line == change.m_range.end.line) {
// simply replace existing line content
line.replace(change.m_range.start.column,
change.m_range.end.column-change.m_range.start.column,
change.m_newText);
} else {
// replace first line contents
line.replace(change.m_range.start.column, line.length() - change.m_range.start.column,
change.m_newText);
// null other lines and remember for deletion
for(int i = change.m_range.start.line + 1; i <= change.m_range.end.line; ++i) {
textLines[i].clear();
removedLines << i;
}
}
}else{
QString warningString = QString("Inconsistent change in %1 at %2:%3 -> %4:%5"
" = \"%6\"(encountered \"%7\") -> \"%8\"")
.arg(file.str())
.arg(change.m_range.start.line)
.arg(change.m_range.start.column)
.arg(change.m_range.end.line)
.arg(change.m_range.end.column)
.arg(change.m_oldText)
.arg(encountered)
.arg(change.m_newText);
if(replacePolicy == DocumentChangeSet::IgnoreFailedChange) {
//Just don't do the replacement
} else if(replacePolicy == DocumentChangeSet::WarnOnFailedChange) {
kWarning() << warningString;
} else {
return DocumentChangeSet::ChangeResult(warningString, sortedChanges[pos]);
}
}
}
if (!removedLines.isEmpty()) {
int offset = 0;
qSort(removedLines);
foreach(int l, removedLines) {
textLines.removeAt(l - offset);
++offset;
}
}
output = textLines.join("\n");
return true;
}
//Removes all duplicate changes for a single file, and then returns (via filteredChanges) the filtered duplicates
DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::removeDuplicates(const IndexedString& file,
ChangesList& filteredChanges)
{
typedef QMultiMap<SimpleCursor, DocumentChangePointer> ChangesMap;
ChangesMap sortedChanges;
foreach(const DocumentChangePointer &change, changes[file]) {
sortedChanges.insert(change->m_range.end, change);
}
//Remove duplicates
ChangesMap::iterator previous = sortedChanges.begin();
for(ChangesMap::iterator it = ++sortedChanges.begin(); it != sortedChanges.end(); ) {
if(( *previous ) && ( *previous )->m_range.end > (*it)->m_range.start) {
//intersection
if(duplicateChanges(( *previous ), *it)) {
//duplicate, remove one
it = sortedChanges.erase(it);
continue;
}
//When two changes contain each other, and the container change is set to ignore old text, then it should be safe to
//just ignore the contained change, and apply the bigger change
else if((*it)->m_range.contains(( *previous )->m_range) && (*it)->m_ignoreOldText ) {
kDebug() << "Removing change: " << ( *previous )->m_oldText << "->" << ( *previous )->m_newText
<< ", because it is contained by change: " << (*it)->m_oldText << "->" << (*it)->m_newText;
sortedChanges.erase(previous);
}
//This case is for when both have the same end, either of them could be the containing range
else if((*previous)->m_range.contains((*it)->m_range) && (*previous)->m_ignoreOldText ) {
kDebug() << "Removing change: " << (*it)->m_oldText << "->" << (*it)->m_newText
<< ", because it is contained by change: " << ( *previous )->m_oldText
<< "->" << ( *previous )->m_newText;
it = sortedChanges.erase(it);
continue;
} else {
return DocumentChangeSet::ChangeResult(
QString("Inconsistent change-request at %1; "
"intersecting changes: "
"\"%2\"->\"%3\"@%4:%5->%6:%7 & \"%8\"->\"%9\"@%10:%11->%12:%13 ")
.arg(file.str(), ( *previous )->m_oldText, ( *previous )->m_newText)
.arg(( *previous )->m_range.start.line)
.arg(( *previous )->m_range.start.column)
.arg(( *previous )->m_range.end.line)
.arg(( *previous )->m_range.end.column)
.arg((*it)->m_oldText, (*it)->m_newText)
.arg((*it)->m_range.start.line)
.arg((*it)->m_range.start.column)
.arg((*it)->m_range.end.line)
.arg((*it)->m_range.end.column));
}
}
previous = it;
++it;
}
filteredChanges = sortedChanges.values();
return true;
}
void DocumentChangeSetPrivate::updateFiles()
{
ModificationRevisionSet::clearCache();
foreach(const IndexedString& file, changes.keys()) {
ModificationRevision::clearModificationCache(file);
}
if(updatePolicy != DocumentChangeSet::NoUpdate && ICore::self())
{
// The active document should be updated first, so that the user sees the results instantly
if(IDocument* activeDoc = ICore::self()->documentController()->activeDocument()) {
ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(activeDoc->url()));
}
// If there are currently open documents that now need an update, update them too
foreach(const IndexedString& doc, ICore::self()->languageController()->backgroundParser()->managedDocuments()) {
DUChainReadLocker lock(DUChain::lock());
TopDUContext* top = DUChainUtils::standardContextForUrl(doc.toUrl(), true);
if((top && top->parsingEnvironmentFile() && top->parsingEnvironmentFile()->needsUpdate()) || !top) {
lock.unlock();
ICore::self()->languageController()->backgroundParser()->addDocument(doc);
}
}
// Eventually update _all_ affected files
foreach(const IndexedString &file, changes.keys()) {
if(!file.toUrl().isValid()) {
kWarning() << "Trying to apply changes to an invalid document";
continue;
}
ICore::self()->languageController()->backgroundParser()->addDocument(file);
}
}
}
}