File indexing completed on 2024-05-12 04:37:47
0001 /* 0002 SPDX-FileCopyrightText: 2008 David Nolden <david.nolden.kdevelop@art-master.de> 0003 0004 SPDX-License-Identifier: LGPL-2.0-only 0005 */ 0006 0007 #include "documentchangeset.h" 0008 0009 #include "coderepresentation.h" 0010 #include <debug.h> 0011 0012 #include <algorithm> 0013 0014 #include <QStringList> 0015 0016 #include <KLocalizedString> 0017 0018 #include <interfaces/icore.h> 0019 #include <interfaces/ilanguagecontroller.h> 0020 #include <interfaces/idocumentcontroller.h> 0021 0022 #include <language/duchain/duchain.h> 0023 #include <language/duchain/duchainlock.h> 0024 #include <language/duchain/duchainutils.h> 0025 #include <language/duchain/parsingenvironment.h> 0026 #include <language/backgroundparser/backgroundparser.h> 0027 #include <language/editor/modificationrevisionset.h> 0028 0029 #include <interfaces/isourceformattercontroller.h> 0030 #include <interfaces/iproject.h> 0031 #include <interfaces/iprojectcontroller.h> 0032 0033 #include <project/projectmodel.h> 0034 0035 #include <util/path.h> 0036 #include <util/shellutils.h> 0037 0038 namespace KDevelop { 0039 using ChangesList = QList<DocumentChangePointer>; 0040 using ChangesHash = QHash<IndexedString, ChangesList>; 0041 0042 class DocumentChangeSetPrivate 0043 { 0044 public: 0045 DocumentChangeSet::ReplacementPolicy replacePolicy; 0046 DocumentChangeSet::FormatPolicy formatPolicy; 0047 DocumentChangeSet::DUChainUpdateHandling updatePolicy; 0048 DocumentChangeSet::ActivationPolicy activationPolicy; 0049 0050 ChangesHash changes; 0051 QHash<IndexedString, IndexedString> documentsRename; 0052 0053 DocumentChangeSet::ChangeResult addChange(const DocumentChangePointer& change); 0054 DocumentChangeSet::ChangeResult replaceOldText(CodeRepresentation* repr, const QString& newText, 0055 const ChangesList& sortedChangesList); 0056 DocumentChangeSet::ChangeResult generateNewText(const IndexedString& file, 0057 ChangesList& sortedChanges, 0058 const CodeRepresentation* repr, 0059 QString& output); 0060 DocumentChangeSet::ChangeResult removeDuplicates(const IndexedString& file, 0061 ChangesList& filteredChanges); 0062 void formatChanges(); 0063 void updateFiles(); 0064 }; 0065 0066 // Simple helpers to clear up code clutter 0067 namespace { 0068 inline bool changeIsValid(const DocumentChange& change, const QStringList& textLines) 0069 { 0070 return change.m_range.start() <= change.m_range.end() && 0071 change.m_range.end().line() < textLines.size() && 0072 change.m_range.start().line() >= 0 && 0073 change.m_range.start().column() >= 0 && 0074 change.m_range.start().column() <= textLines[change.m_range.start().line()].length() && 0075 change.m_range.end().column() >= 0 && 0076 change.m_range.end().column() <= textLines[change.m_range.end().line()].length(); 0077 } 0078 0079 inline bool duplicateChanges(const DocumentChangePointer& previous, const DocumentChangePointer& current) 0080 { 0081 // Given the option of considering a duplicate two changes in the same range 0082 // but with different old texts to be ignored 0083 return previous->m_range == current->m_range && 0084 previous->m_newText == current->m_newText && 0085 (previous->m_oldText == current->m_oldText || 0086 (previous->m_ignoreOldText && current->m_ignoreOldText)); 0087 } 0088 0089 inline QString rangeText(const KTextEditor::Range& range, const QStringList& textLines) 0090 { 0091 QStringList ret; 0092 ret.reserve(range.end().line() - range.start().line() + 1); 0093 for (int line = range.start().line(); line <= range.end().line(); ++line) { 0094 const QString lineText = textLines.at(line); 0095 int startColumn = 0; 0096 int endColumn = lineText.length(); 0097 if (line == range.start().line()) { 0098 startColumn = range.start().column(); 0099 } 0100 if (line == range.end().line()) { 0101 endColumn = range.end().column(); 0102 } 0103 ret << lineText.mid(startColumn, endColumn - startColumn); 0104 } 0105 0106 return ret.join(QLatin1Char('\n')); 0107 } 0108 0109 // need to have it as otherwise the arguments can exceed the maximum of 10 0110 static QString printRange(const KTextEditor::Range& r) 0111 { 0112 return i18nc("text range line:column->line:column", "%1:%2->%3:%4", 0113 r.start().line(), r.start().column(), 0114 r.end().line(), r.end().column()); 0115 } 0116 } 0117 0118 DocumentChangeSet::DocumentChangeSet() 0119 : d_ptr(new DocumentChangeSetPrivate) 0120 { 0121 Q_D(DocumentChangeSet); 0122 0123 d->replacePolicy = StopOnFailedChange; 0124 d->formatPolicy = AutoFormatChanges; 0125 d->updatePolicy = SimpleUpdate; 0126 d->activationPolicy = DoNotActivate; 0127 } 0128 0129 DocumentChangeSet::DocumentChangeSet(const DocumentChangeSet& rhs) 0130 : d_ptr(new DocumentChangeSetPrivate(*rhs.d_ptr)) 0131 { 0132 } 0133 0134 DocumentChangeSet& DocumentChangeSet::operator=(const DocumentChangeSet& rhs) 0135 { 0136 *d_ptr = *rhs.d_ptr; 0137 return *this; 0138 } 0139 0140 DocumentChangeSet::~DocumentChangeSet() = default; 0141 0142 DocumentChangeSet::ChangeResult DocumentChangeSet::addChange(const DocumentChange& change) 0143 { 0144 Q_D(DocumentChangeSet); 0145 0146 return d->addChange(DocumentChangePointer(new DocumentChange(change))); 0147 } 0148 0149 DocumentChangeSet::ChangeResult DocumentChangeSet::addChange(const DocumentChangePointer& change) 0150 { 0151 Q_D(DocumentChangeSet); 0152 0153 return d->addChange(change); 0154 } 0155 0156 DocumentChangeSet::ChangeResult DocumentChangeSet::addDocumentRenameChange(const IndexedString& oldFile, 0157 const IndexedString& newname) 0158 { 0159 Q_D(DocumentChangeSet); 0160 0161 d->documentsRename.insert(oldFile, newname); 0162 return DocumentChangeSet::ChangeResult::successfulResult(); 0163 } 0164 0165 DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::addChange(const DocumentChangePointer& change) 0166 { 0167 changes[change->m_document].append(change); 0168 return DocumentChangeSet::ChangeResult::successfulResult(); 0169 } 0170 0171 void DocumentChangeSet::setReplacementPolicy(DocumentChangeSet::ReplacementPolicy policy) 0172 { 0173 Q_D(DocumentChangeSet); 0174 0175 d->replacePolicy = policy; 0176 } 0177 0178 void DocumentChangeSet::setFormatPolicy(DocumentChangeSet::FormatPolicy policy) 0179 { 0180 Q_D(DocumentChangeSet); 0181 0182 d->formatPolicy = policy; 0183 } 0184 0185 void DocumentChangeSet::setUpdateHandling(DocumentChangeSet::DUChainUpdateHandling policy) 0186 { 0187 Q_D(DocumentChangeSet); 0188 0189 d->updatePolicy = policy; 0190 } 0191 0192 void DocumentChangeSet::setActivationPolicy(DocumentChangeSet::ActivationPolicy policy) 0193 { 0194 Q_D(DocumentChangeSet); 0195 0196 d->activationPolicy = policy; 0197 } 0198 0199 DocumentChangeSet::ChangeResult DocumentChangeSet::applyAllChanges() 0200 { 0201 Q_D(DocumentChangeSet); 0202 0203 QUrl oldActiveDoc; 0204 if (IDocument* activeDoc = ICore::self()->documentController()->activeDocument()) { 0205 oldActiveDoc = activeDoc->url(); 0206 } 0207 0208 QList<QUrl> allFiles; 0209 const auto changedFiles = QSet<KDevelop::IndexedString>(d->documentsRename.keyBegin(), d->documentsRename.keyEnd()) 0210 + QSet<KDevelop::IndexedString>(d->changes.keyBegin(), d->changes.keyEnd()); 0211 allFiles.reserve(changedFiles.size()); 0212 for (const IndexedString& file : changedFiles) { 0213 allFiles << file.toUrl(); 0214 } 0215 0216 if (!KDevelop::ensureWritable(allFiles)) { 0217 return ChangeResult(QStringLiteral("some affected files are not writable")); 0218 } 0219 0220 // rename files 0221 QHash<IndexedString, IndexedString>::const_iterator it = d->documentsRename.constBegin(); 0222 for (; it != d->documentsRename.constEnd(); ++it) { 0223 QUrl url = it.key().toUrl(); 0224 IProject* p = ICore::self()->projectController()->findProjectForUrl(url); 0225 if (p) { 0226 QList<ProjectFileItem*> files = p->filesForPath(it.key()); 0227 if (!files.isEmpty()) { 0228 ProjectBaseItem::RenameStatus renamed = files.first()->rename(it.value().str()); 0229 if (renamed == ProjectBaseItem::RenameOk) { 0230 const QUrl newUrl = Path(Path(url).parent(), it.value().str()).toUrl(); 0231 if (url == oldActiveDoc) { 0232 oldActiveDoc = newUrl; 0233 } 0234 IndexedString idxNewDoc(newUrl); 0235 0236 // ensure changes operate on new file name 0237 ChangesHash::iterator iter = d->changes.find(it.key()); 0238 if (iter != d->changes.end()) { 0239 // copy changes 0240 ChangesList value = iter.value(); 0241 // remove old entry 0242 d->changes.erase(iter); 0243 // adapt to new url 0244 for (auto& change : value) { 0245 change->m_document = idxNewDoc; 0246 } 0247 0248 d->changes[idxNewDoc] = value; 0249 } 0250 } else { 0251 ///FIXME: share code with project manager for the error code string representation 0252 return ChangeResult(i18n("Could not rename '%1' to '%2'", 0253 url.toDisplayString(QUrl::PreferLocalFile), it.value().str())); 0254 } 0255 } else { 0256 //TODO: do it outside the project management? 0257 qCWarning(LANGUAGE) << "tried to rename file not tracked by project - not implemented"; 0258 } 0259 } else { 0260 qCWarning(LANGUAGE) << "tried to rename a file outside of a project - not implemented"; 0261 } 0262 } 0263 0264 QMap<IndexedString, CodeRepresentation::Ptr> codeRepresentations; 0265 QMap<IndexedString, QString> newTexts; 0266 ChangesHash filteredSortedChanges; 0267 ChangeResult result = ChangeResult::successfulResult(); 0268 0269 const QList<IndexedString> files(d->changes.keys()); 0270 0271 for (const IndexedString& file : files) { 0272 CodeRepresentation::Ptr repr = createCodeRepresentation(file); 0273 if (!repr) { 0274 return ChangeResult(QStringLiteral("Could not create a Representation for %1").arg(file.str())); 0275 } 0276 0277 codeRepresentations[file] = repr; 0278 0279 QList<DocumentChangePointer>& sortedChangesList(filteredSortedChanges[file]); 0280 { 0281 result = d->removeDuplicates(file, sortedChangesList); 0282 if (!result) 0283 return result; 0284 } 0285 0286 { 0287 result = d->generateNewText(file, sortedChangesList, repr.data(), newTexts[file]); 0288 if (!result) 0289 return result; 0290 } 0291 } 0292 0293 QMap<IndexedString, QString> oldTexts; 0294 0295 //Apply the changes to the files 0296 for (const IndexedString& file : files) { 0297 oldTexts[file] = codeRepresentations[file]->text(); 0298 0299 result = d->replaceOldText(codeRepresentations[file].data(), newTexts[file], filteredSortedChanges[file]); 0300 if (!result && d->replacePolicy == StopOnFailedChange) { 0301 //Revert all files 0302 for (auto it = oldTexts.constBegin(), end = oldTexts.constEnd(); it != end; ++it) { 0303 const IndexedString& revertFile = it.key(); 0304 const QString& oldText = it.value(); 0305 codeRepresentations[revertFile]->setText(oldText); 0306 } 0307 0308 return result; 0309 } 0310 } 0311 0312 d->updateFiles(); 0313 0314 if (d->activationPolicy == Activate) { 0315 for (const IndexedString& file : files) { 0316 ICore::self()->documentController()->openDocument(file.toUrl()); 0317 } 0318 } 0319 0320 // ensure the old document is still activated 0321 if (oldActiveDoc.isValid()) { 0322 ICore::self()->documentController()->openDocument(oldActiveDoc); 0323 } 0324 0325 return result; 0326 } 0327 0328 DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::replaceOldText(CodeRepresentation* repr, 0329 const QString& newText, 0330 const ChangesList& sortedChangesList) 0331 { 0332 auto* dynamic = dynamic_cast<DynamicCodeRepresentation*>(repr); 0333 if (dynamic) { 0334 auto transaction = dynamic->makeEditTransaction(); 0335 //Replay the changes one by one 0336 0337 for (int pos = sortedChangesList.size() - 1; pos >= 0; --pos) { 0338 const DocumentChange& change(*sortedChangesList[pos]); 0339 if (!dynamic->replace(change.m_range, change.m_oldText, change.m_newText, change.m_ignoreOldText)) { 0340 QString warningString = i18nc( 0341 "Inconsistent change in <filename> between <range>, found <oldText> (encountered <foundText>) -> <newText>", 0342 "Inconsistent change in %1 between %2, found %3 (encountered \"%4\") -> \"%5\"", 0343 change.m_document.str(), 0344 printRange(change.m_range), 0345 change.m_oldText, 0346 dynamic->rangeText(change.m_range), 0347 change.m_newText); 0348 0349 if (replacePolicy == DocumentChangeSet::WarnOnFailedChange) { 0350 qCWarning(LANGUAGE) << warningString; 0351 } else if (replacePolicy == DocumentChangeSet::StopOnFailedChange) { 0352 return DocumentChangeSet::ChangeResult(warningString); 0353 } 0354 //If set to ignore failed changes just continue with the others 0355 } 0356 } 0357 0358 return DocumentChangeSet::ChangeResult::successfulResult(); 0359 } 0360 0361 //For files on disk 0362 if (!repr->setText(newText)) { 0363 QString warningString = i18n("Could not replace text in the document: %1", 0364 sortedChangesList.begin()->data()->m_document.str()); 0365 if (replacePolicy == DocumentChangeSet::WarnOnFailedChange) { 0366 qCWarning(LANGUAGE) << warningString; 0367 } 0368 0369 return DocumentChangeSet::ChangeResult(warningString); 0370 } 0371 0372 return DocumentChangeSet::ChangeResult::successfulResult(); 0373 } 0374 0375 DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::generateNewText(const IndexedString& file, 0376 ChangesList& sortedChanges, 0377 const CodeRepresentation* repr, 0378 QString& output) 0379 { 0380 //Create the actual new modified file 0381 QStringList textLines = repr->text().split(QLatin1Char('\n')); 0382 0383 ISourceFormatterController::FileFormatterPtr formatter; 0384 if (formatPolicy != DocumentChangeSet::NoAutoFormat) { 0385 formatter = ICore::self()->sourceFormatterController()->fileFormatter(file.toUrl()); 0386 } 0387 0388 QVector<int> removedLines; 0389 0390 for (int pos = sortedChanges.size() - 1; pos >= 0; --pos) { 0391 DocumentChange& change(*sortedChanges[pos]); 0392 QString encountered; 0393 if (changeIsValid(change, textLines) && //We demand this, although it should be fixed 0394 ((encountered = rangeText(change.m_range, textLines)) == change.m_oldText || change.m_ignoreOldText)) { 0395 ///Problem: This does not work if the other changes significantly alter the context @todo Use the changed context 0396 QString leftContext = QStringList(textLines.mid(0, change.m_range.start().line() + 1)).join(QLatin1Char( 0397 '\n')); 0398 leftContext.chop(textLines[change.m_range.start().line()].length() - change.m_range.start().column()); 0399 0400 QString rightContext = QStringList(textLines.mid(change.m_range.end().line())).join(QLatin1Char('\n')).mid( 0401 change.m_range.end().column()); 0402 0403 if (formatter) { 0404 QString oldNewText = change.m_newText; 0405 change.m_newText = formatter->format(change.m_newText, leftContext, rightContext); 0406 0407 if (formatPolicy == DocumentChangeSet::AutoFormatChangesKeepIndentation) { 0408 // Reproduce the previous indentation 0409 const QStringList oldLines = oldNewText.split(QLatin1Char('\n')); 0410 QStringList newLines = change.m_newText.split(QLatin1Char('\n')); 0411 0412 if (oldLines.size() == newLines.size()) { 0413 for (int line = 0; line < newLines.size(); ++line) { 0414 // Keep the previous indentation 0415 QString oldIndentation; 0416 for (const QChar a : oldLines[line]) { 0417 if (a.isSpace()) { 0418 oldIndentation.append(a); 0419 } else { 0420 break; 0421 } 0422 } 0423 0424 int newIndentationLength = 0; 0425 0426 for (int a = 0; a < newLines[line].size(); ++a) { 0427 if (newLines[line][a].isSpace()) { 0428 newIndentationLength = a; 0429 } else { 0430 break; 0431 } 0432 } 0433 0434 newLines[line].replace(0, newIndentationLength, oldIndentation); 0435 } 0436 0437 change.m_newText = newLines.join(QLatin1Char('\n')); 0438 } else { 0439 qCDebug(LANGUAGE) << "Cannot keep the indentation because the line count has changed" << 0440 oldNewText; 0441 } 0442 } 0443 } 0444 0445 QString& line = textLines[change.m_range.start().line()]; 0446 if (change.m_range.start().line() == change.m_range.end().line()) { 0447 // simply replace existing line content 0448 line.replace(change.m_range.start().column(), 0449 change.m_range.end().column() - change.m_range.start().column(), 0450 change.m_newText); 0451 } else { 0452 // replace first line contents 0453 line.replace(change.m_range.start().column(), line.length() - change.m_range.start().column(), 0454 change.m_newText); 0455 // null other lines and remember for deletion 0456 const int firstLine = change.m_range.start().line() + 1; 0457 const int lastLine = change.m_range.end().line(); 0458 removedLines.reserve(removedLines.size() + lastLine - firstLine + 1); 0459 for (int i = firstLine; i <= lastLine; ++i) { 0460 textLines[i].clear(); 0461 removedLines << i; 0462 } 0463 } 0464 } else { 0465 QString warningString = i18nc("Inconsistent change in <document> at <range>" 0466 " = <oldText> (encountered <encountered>) -> <newText>", 0467 "Inconsistent change in %1 at %2" 0468 " = \"%3\"(encountered \"%4\") -> \"%5\"", 0469 file.str(), 0470 printRange(change.m_range), 0471 change.m_oldText, 0472 encountered, 0473 change.m_newText); 0474 0475 if (replacePolicy == DocumentChangeSet::IgnoreFailedChange) { 0476 //Just don't do the replacement 0477 } else if (replacePolicy == DocumentChangeSet::WarnOnFailedChange) { 0478 qCWarning(LANGUAGE) << warningString; 0479 } else { 0480 return DocumentChangeSet::ChangeResult(warningString, sortedChanges[pos]); 0481 } 0482 } 0483 } 0484 0485 if (!removedLines.isEmpty()) { 0486 int offset = 0; 0487 std::sort(removedLines.begin(), removedLines.end()); 0488 for (int l : qAsConst(removedLines)) { 0489 textLines.removeAt(l - offset); 0490 ++offset; 0491 } 0492 } 0493 output = textLines.join(QLatin1Char('\n')); 0494 return DocumentChangeSet::ChangeResult::successfulResult(); 0495 } 0496 0497 //Removes all duplicate changes for a single file, and then returns (via filteredChanges) the filtered duplicates 0498 DocumentChangeSet::ChangeResult DocumentChangeSetPrivate::removeDuplicates(const IndexedString& file, 0499 ChangesList& filteredChanges) 0500 { 0501 using ChangesMap = QMultiMap<KTextEditor::Cursor, DocumentChangePointer>; 0502 ChangesMap sortedChanges; 0503 0504 for (const DocumentChangePointer& change : qAsConst(changes[file])) { 0505 sortedChanges.insert(change->m_range.end(), change); 0506 } 0507 0508 //Remove duplicates 0509 ChangesMap::iterator previous = sortedChanges.begin(); 0510 for (ChangesMap::iterator it = ++sortedChanges.begin(); it != sortedChanges.end();) { 0511 if ((*previous) && (*previous)->m_range.end() > (*it)->m_range.start()) { 0512 //intersection 0513 if (duplicateChanges((*previous), *it)) { 0514 //duplicate, remove one 0515 it = sortedChanges.erase(it); 0516 continue; 0517 } 0518 //When two changes contain each other, and the container change is set to ignore old text, then it should be safe to 0519 //just ignore the contained change, and apply the bigger change 0520 else if ((*it)->m_range.contains((*previous)->m_range) && (*it)->m_ignoreOldText) { 0521 qCDebug(LANGUAGE) << "Removing change: " << (*previous)->m_oldText << "->" << (*previous)->m_newText 0522 << ", because it is contained by change: " << (*it)->m_oldText << "->" << 0523 (*it)->m_newText; 0524 sortedChanges.erase(previous); 0525 } 0526 //This case is for when both have the same end, either of them could be the containing range 0527 else if ((*previous)->m_range.contains((*it)->m_range) && (*previous)->m_ignoreOldText) { 0528 qCDebug(LANGUAGE) << "Removing change: " << (*it)->m_oldText << "->" << (*it)->m_newText 0529 << ", because it is contained by change: " << (*previous)->m_oldText 0530 << "->" << (*previous)->m_newText; 0531 it = sortedChanges.erase(it); 0532 continue; 0533 } else { 0534 return DocumentChangeSet::ChangeResult( 0535 i18nc("Inconsistent change-request at <document>:" 0536 "intersecting changes: " 0537 "<previous-oldText> -> <previous-newText> @<range> & " 0538 "<new-oldText> -> <new-newText> @<range>", 0539 "Inconsistent change-request at %1; " 0540 "intersecting changes: " 0541 "\"%2\"->\"%3\"@%4 & \"%5\"->\"%6\"@%7 ", 0542 file.str(), 0543 (*previous)->m_oldText, 0544 (*previous)->m_newText, 0545 printRange((*previous)->m_range), 0546 (*it)->m_oldText, 0547 (*it)->m_newText, 0548 printRange((*it)->m_range))); 0549 } 0550 } 0551 previous = it; 0552 ++it; 0553 } 0554 0555 filteredChanges = sortedChanges.values(); 0556 return DocumentChangeSet::ChangeResult::successfulResult(); 0557 } 0558 0559 void DocumentChangeSetPrivate::updateFiles() 0560 { 0561 ModificationRevisionSet::clearCache(); 0562 const auto files = changes.keys(); 0563 for (const IndexedString& file : files) { 0564 ModificationRevision::clearModificationCache(file); 0565 } 0566 0567 if (updatePolicy != DocumentChangeSet::NoUpdate && ICore::self()) { 0568 // The active document should be updated first, so that the user sees the results instantly 0569 if (IDocument* activeDoc = ICore::self()->documentController()->activeDocument()) { 0570 ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(activeDoc->url())); 0571 } 0572 0573 // If there are currently open documents that now need an update, update them too 0574 const auto documents = ICore::self()->languageController()->backgroundParser()->managedDocuments(); 0575 for (const IndexedString& doc : documents) { 0576 DUChainReadLocker lock(DUChain::lock()); 0577 TopDUContext* top = DUChainUtils::standardContextForUrl(doc.toUrl(), true); 0578 if ((top && top->parsingEnvironmentFile() && top->parsingEnvironmentFile()->needsUpdate()) || !top) { 0579 lock.unlock(); 0580 ICore::self()->languageController()->backgroundParser()->addDocument(doc); 0581 } 0582 } 0583 0584 // Eventually update _all_ affected files 0585 const auto files = changes.keys(); 0586 for (const IndexedString& file : files) { 0587 if (!file.toUrl().isValid()) { 0588 qCWarning(LANGUAGE) << "Trying to apply changes to an invalid document"; 0589 continue; 0590 } 0591 0592 ICore::self()->languageController()->backgroundParser()->addDocument(file); 0593 } 0594 } 0595 } 0596 }