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 }