diff --git a/language/codegen/documentchangeset.cpp b/language/codegen/documentchangeset.cpp index 24ff2f40d7..74de81deb3 100644 --- a/language/codegen/documentchangeset.cpp +++ b/language/codegen/documentchangeset.cpp @@ -1,458 +1,461 @@ /* Copyright 2008 David Nolden 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace KDevelop { struct DocumentChangeSetPrivate { DocumentChangeSet::ReplacementPolicy replacePolicy; DocumentChangeSet::FormatPolicy formatPolicy; DocumentChangeSet::DUChainUpdateHandling updatePolicy; DocumentChangeSet::ActivationPolicy activationPolicy; QMap< IndexedString, QList > changes; DocumentChangeSet::ChangeResult addChange(DocumentChangePointer change); DocumentChangeSet::ChangeResult replaceOldText(CodeRepresentation * repr, const QString & newText, const QList & sortedChangesList); DocumentChangeSet::ChangeResult generateNewText(const KDevelop::IndexedString & file, QList< KDevelop::DocumentChangePointer > & sortedChanges, const KDevelop::CodeRepresentation* repr, QString& output); DocumentChangeSet::ChangeResult removeDuplicates(const IndexedString & file, QList & filteredChanges); void formatChanges(); void updateFiles(); }; //Simple helper 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() && change.m_range.start.line == change.m_range.end.line; } inline bool duplicateChanges(DocumentChangePointer previous, 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)); } } 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 KDevelop::DocumentChangeSet& rhs) { *d = *rhs.d; return *this; } DocumentChangeSet::~DocumentChangeSet() { delete d; } KDevelop::DocumentChangeSet::ChangeResult DocumentChangeSet::addChange(const KDevelop::DocumentChange& change) { return d->addChange(DocumentChangePointer(new DocumentChange(change))); } DocumentChangeSet::ChangeResult DocumentChangeSet::addChange(DocumentChangePointer change) { return d->addChange(change); } DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::addChange(DocumentChangePointer change) { if(change->m_range.start.line != change->m_range.end.line) + { + kWarning() << "Multi-line changes are not supported in DocumentChangeSet"; return DocumentChangeSet::ChangeResult("Multi-line ranges are not supported"); + } 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; } QMap< IndexedString, InsertArtificialCodeRepresentationPointer > DocumentChangeSet::temporaryCodeRepresentations() const { QMap< IndexedString, InsertArtificialCodeRepresentationPointer > ret; ChangeResult result(true); foreach(const IndexedString &file, d->changes.keys()) { CodeRepresentation::Ptr repr = createCodeRepresentation(file); if(!repr) continue; QList sortedChangesList; result = d->removeDuplicates(file, sortedChangesList); if(!result) continue; QString newText; result = d->generateNewText(file, sortedChangesList, repr.data(), newText); if(!result) continue; InsertArtificialCodeRepresentationPointer code( new InsertArtificialCodeRepresentation(IndexedString(file.toUrl().fileName()), newText) ); ret.insert(file, code); } return ret; } DocumentChangeSet::ChangeResult DocumentChangeSet::applyAllChanges() { QMap codeRepresentations; QMap newTexts; QMap > filteredSortedChanges; ChangeResult result(true); QList 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& 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 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()); return result; } DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::replaceOldText(CodeRepresentation * repr, const QString & newText, const QList & sortedChangesList) { DynamicCodeRepresentation* dynamic = dynamic_cast(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, QList & 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); 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 = textLines[change.m_range.start.line].mid(change.m_range.start.column, change.m_range.end.column-change.m_range.start.column)) == 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; } } } textLines[change.m_range.start.line].replace(change.m_range.start.column, change.m_range.end.column-change.m_range.start.column, change.m_newText); }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]); } } 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, QList & filteredChanges) { QMultiMap sortedChanges; foreach(const DocumentChangePointer &change, changes[file]) sortedChanges.insert(change->m_range.end, change); //Remove duplicates QMultiMap::iterator previous = sortedChanges.begin(); for(QMultiMap::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(ICore::self()->documentController()->activeDocument()) ICore::self()->languageController()->backgroundParser()->addDocument(ICore::self()->documentController()->activeDocument()->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.toUrl()); } } // 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.toUrl()); } } } } diff --git a/language/codegen/documentchangeset.h b/language/codegen/documentchangeset.h index 6b58fe5d54..5d99d97e33 100644 --- a/language/codegen/documentchangeset.h +++ b/language/codegen/documentchangeset.h @@ -1,135 +1,136 @@ /* Copyright 2008 David Nolden 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. */ #ifndef DOCUMENTCHANGESET_H #define DOCUMENTCHANGESET_H #include #include #include #include #include "coderepresentation.h" namespace KDevelop { struct DocumentChangeSetPrivate; class KDEVPLATFORMLANGUAGE_EXPORT DocumentChange : public QSharedData { public: DocumentChange(const IndexedString& document, const SimpleRange& range, const QString& oldText, const QString& newText) : m_document(document), m_range(range), m_oldText(oldText), m_newText(newText), m_ignoreOldText(false) { //Clean the URL, so we don't get the same file be stored as a different one KUrl url(m_document.toUrl()); url.cleanPath(); m_document = IndexedString(url); } IndexedString m_document; SimpleRange m_range; QString m_oldText; QString m_newText; bool m_ignoreOldText; //Set this to disable the verification of m_oldText. This can be used to overwrite arbitrary text, but is dangerous! }; typedef KSharedPtr DocumentChangePointer; /** * Object representing an arbitrary set of changes to an arbitrary set of files that can be applied atomically. */ class KDEVPLATFORMLANGUAGE_EXPORT DocumentChangeSet { public: DocumentChangeSet(); ~DocumentChangeSet(); DocumentChangeSet(const DocumentChangeSet & rhs); DocumentChangeSet& operator=(const DocumentChangeSet& rhs); //Returns true on success struct ChangeResult { ChangeResult(bool success) : m_success(success) { } ChangeResult(QString failureReason, DocumentChangePointer reasonChange = DocumentChangePointer()) : m_failureReason(failureReason), m_reasonChange(reasonChange), m_success(false) { } operator bool() const { return m_success; } /// Reason why the change failed QString m_failureReason; /// Specific change that caused the problem (might be 0) DocumentChangePointer m_reasonChange; bool m_success; }; - ///If the change has multiple lines, a problem will be returned. these don't work at he moment. + /// Add an individual local change to this change-set. + ///@note Multi-line changes are not (yet) supported. ChangeResult addChange(const DocumentChange& change); ChangeResult addChange(DocumentChangePointer change); enum ReplacementPolicy { IgnoreFailedChange,///If this is given, all changes that could not be applied are simply ignored WarnOnFailedChange,///If this is given to applyAllChanges, a warning is given when a change could not be applied, ///but following changes are applied, and success is returned. StopOnFailedChange ///If this is given to applyAllChanges, then all replacements are reverted and an error returned on problems (default) }; ///@param policy What should be done when a change could not be applied? void setReplacementPolicy(ReplacementPolicy policy); enum FormatPolicy { NoAutoFormat, ///If this option is given, no automatic formatting is applied AutoFormatChanges, ///If this option is given, all changes are automatically reformatted using the formatter plugin for the mime type (default) AutoFormatChangesKeepIndentation ///Same as AutoFormatChanges, except that the indentation of inserted lines is kept equal }; ///@param policy How the changed text should be formatted. The default is AutoFormatChanges. void setFormatPolicy(FormatPolicy policy); enum DUChainUpdateHandling { NoUpdate, ///No updates will be scheduled SimpleUpdate ///The changed documents will be added to the background parser, plus all documents that are currently open and recursively import those documents (default) //FullUpdate ///All documents in all open projects that recursively import any of the changed documents will be updated }; ///@param policy Whether a duchain update should be triggered for all affected documents void setUpdateHandling(DUChainUpdateHandling policy); enum ActivationPolicy { Activate, ///The affected files will be activated DoNotActivate ///The affected files will not be activated (default) }; ///@param policy Whether the affected documents should be activated when the change is applied void setActivationPolicy(ActivationPolicy policy); ///Applies all changes to temporary code representations, and returns a map from each file-name to ///the respective inserted artificial code-representation. QMap temporaryCodeRepresentations() const; /// Apply all the changes registered in this changeset to the actual files ChangeResult applyAllChanges(); private: DocumentChangeSetPrivate * d; }; } #endif