/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ #include "cppfindreferences.h" #include "cppfilesettingspage.h" #include "cpptoolsconstants.h" #include "cppmodelmanager.h" #include "cppworkingcopy.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Core; using namespace CppTools::Internal; using namespace CppTools; using namespace CPlusPlus; using namespace ProjectExplorer; static QByteArray getSource(const Utils::FileName &fileName, const WorkingCopy &workingCopy) { if (workingCopy.contains(fileName)) { return workingCopy.source(fileName); } else { QString fileContents; Utils::TextFileFormat format; QString error; QTextCodec *defaultCodec = EditorManager::defaultTextCodec(); Utils::TextFileFormat::ReadResult result = Utils::TextFileFormat::readFile( fileName.toString(), defaultCodec, &fileContents, &format, &error); if (result != Utils::TextFileFormat::ReadSuccess) qWarning() << "Could not read " << fileName << ". Error: " << error; return fileContents.toUtf8(); } } static QByteArray typeId(Symbol *symbol) { if (symbol->asEnum()) { return QByteArray("e"); } else if (symbol->asFunction()) { return QByteArray("f"); } else if (symbol->asNamespace()) { return QByteArray("n"); } else if (symbol->asTemplate()) { return QByteArray("t"); } else if (symbol->asNamespaceAlias()) { return QByteArray("na"); } else if (symbol->asClass()) { return QByteArray("c"); } else if (symbol->asBlock()) { return QByteArray("b"); } else if (symbol->asUsingNamespaceDirective()) { return QByteArray("u"); } else if (symbol->asUsingDeclaration()) { return QByteArray("ud"); } else if (symbol->asDeclaration()) { QByteArray temp("d,"); Overview pretty; temp.append(pretty.prettyType(symbol->type()).toUtf8()); return temp; } else if (symbol->asArgument()) { return QByteArray("a"); } else if (symbol->asTypenameArgument()) { return QByteArray("ta"); } else if (symbol->asBaseClass()) { return QByteArray("bc"); } else if (symbol->asForwardClassDeclaration()) { return QByteArray("fcd"); } else if (symbol->asQtPropertyDeclaration()) { return QByteArray("qpd"); } else if (symbol->asQtEnum()) { return QByteArray("qe"); } else if (symbol->asObjCBaseClass()) { return QByteArray("ocbc"); } else if (symbol->asObjCBaseProtocol()) { return QByteArray("ocbp"); } else if (symbol->asObjCClass()) { return QByteArray("occ"); } else if (symbol->asObjCForwardClassDeclaration()) { return QByteArray("ocfd"); } else if (symbol->asObjCProtocol()) { return QByteArray("ocp"); } else if (symbol->asObjCForwardProtocolDeclaration()) { return QByteArray("ocfpd"); } else if (symbol->asObjCMethod()) { return QByteArray("ocm"); } else if (symbol->asObjCPropertyDeclaration()) { return QByteArray("ocpd"); } return QByteArray("unknown"); } static QByteArray idForSymbol(Symbol *symbol) { QByteArray uid(typeId(symbol)); if (const Identifier *id = symbol->identifier()) { uid.append("|"); uid.append(QByteArray(id->chars(), id->size())); } else if (Scope *scope = symbol->enclosingScope()) { // add the index of this symbol within its enclosing scope // (counting symbols without identifier of the same type) int count = 0; Scope::iterator it = scope->memberBegin(); while (it != scope->memberEnd() && *it != symbol) { Symbol *val = *it; ++it; if (val->identifier() || typeId(val) != uid) continue; ++count; } uid.append(QString::number(count).toLocal8Bit()); } return uid; } static QList fullIdForSymbol(Symbol *symbol) { QList uid; Symbol *current = symbol; do { uid.prepend(idForSymbol(current)); current = current->enclosingScope(); } while (current); return uid; } namespace { class ProcessFile: public std::unary_function > { const WorkingCopy workingCopy; const Snapshot snapshot; Document::Ptr symbolDocument; Symbol *symbol; QFutureInterface *future; public: ProcessFile(const WorkingCopy &workingCopy, const Snapshot snapshot, Document::Ptr symbolDocument, Symbol *symbol, QFutureInterface *future) : workingCopy(workingCopy), snapshot(snapshot), symbolDocument(symbolDocument), symbol(symbol), future(future) { } QList operator()(const Utils::FileName &fileName) { QList usages; if (future->isPaused()) future->waitForResume(); if (future->isCanceled()) return usages; const Identifier *symbolId = symbol->identifier(); if (Document::Ptr previousDoc = snapshot.document(fileName)) { Control *control = previousDoc->control(); if (!control->findIdentifier(symbolId->chars(), symbolId->size())) return usages; // skip this document, it's not using symbolId. } Document::Ptr doc; const QByteArray unpreprocessedSource = getSource(fileName, workingCopy); if (symbolDocument && fileName == Utils::FileName::fromString(symbolDocument->fileName())) { doc = symbolDocument; } else { doc = snapshot.preprocessedDocument(unpreprocessedSource, fileName); doc->tokenize(); } Control *control = doc->control(); if (control->findIdentifier(symbolId->chars(), symbolId->size()) != 0) { if (doc != symbolDocument) doc->check(); FindUsages process(unpreprocessedSource, doc, snapshot); process(symbol); usages = process.usages(); } if (future->isPaused()) future->waitForResume(); return usages; } }; class UpdateUI: public std::binary_function &, QList, void> { QFutureInterface *future; public: UpdateUI(QFutureInterface *future): future(future) {} void operator()(QList &, const QList &usages) { foreach (const Usage &u, usages) future->reportResult(u); future->setProgressValue(future->progressValue() + 1); } }; } // end of anonymous namespace CppFindReferences::CppFindReferences(CppModelManager *modelManager) : QObject(modelManager), m_modelManager(modelManager) { } CppFindReferences::~CppFindReferences() { } QList CppFindReferences::references(Symbol *symbol, const LookupContext &context) const { QList references; FindUsages findUsages(context); findUsages(symbol); references = findUsages.references(); return references; } static void find_helper(QFutureInterface &future, const WorkingCopy workingCopy, const LookupContext context, Symbol *symbol) { const Identifier *symbolId = symbol->identifier(); QTC_ASSERT(symbolId != 0, return); const Snapshot snapshot = context.snapshot(); const Utils::FileName sourceFile = Utils::FileName::fromUtf8(symbol->fileName(), symbol->fileNameLength()); Utils::FileNameList files{sourceFile}; if (symbol->isClass() || symbol->isForwardClassDeclaration() || (symbol->enclosingScope() && !symbol->isStatic() && symbol->enclosingScope()->isNamespace())) { const Snapshot snapshotFromContext = context.snapshot(); for (auto i = snapshotFromContext.begin(), ei = snapshotFromContext.end(); i != ei; ++i) { if (i.key() == sourceFile) continue; const Control *control = i.value()->control(); if (control->findIdentifier(symbolId->chars(), symbolId->size())) files.append(i.key()); } } else { files += snapshot.filesDependingOn(sourceFile); } files = Utils::filteredUnique(files); future.setProgressRange(0, files.size()); ProcessFile process(workingCopy, snapshot, context.thisDocument(), symbol, &future); UpdateUI reduce(&future); // This thread waits for blockingMappedReduced to finish, so reduce the pool's used thread count // so the blockingMappedReduced can use one more thread, and increase it again afterwards. QThreadPool::globalInstance()->releaseThread(); QtConcurrent::blockingMappedReduced > (files, process, reduce); QThreadPool::globalInstance()->reserveThread(); future.setProgressValue(files.size()); } void CppFindReferences::findUsages(Symbol *symbol, const LookupContext &context) { findUsages(symbol, context, QString(), false); } void CppFindReferences::findUsages(Symbol *symbol, const LookupContext &context, const QString &replacement, bool replace) { Overview overview; SearchResult *search = SearchResultWindow::instance()->startNewSearch(tr("C++ Usages:"), QString(), overview.prettyName(context.fullyQualifiedName(symbol)), replace ? SearchResultWindow::SearchAndReplace : SearchResultWindow::SearchOnly, SearchResultWindow::PreserveCaseDisabled, QLatin1String("CppEditor")); search->setTextToReplace(replacement); connect(search, &SearchResult::replaceButtonClicked, this, &CppFindReferences::onReplaceButtonClicked); search->setSearchAgainSupported(true); connect(search, &SearchResult::searchAgainRequested, this, &CppFindReferences::searchAgain); CppFindReferencesParameters parameters; parameters.symbolId = fullIdForSymbol(symbol); parameters.symbolFileName = QByteArray(symbol->fileName()); if (symbol->isClass() || symbol->isForwardClassDeclaration()) { Overview overview; parameters.prettySymbolName = overview.prettyName(context.path(symbol).last()); } search->setUserData(qVariantFromValue(parameters)); findAll_helper(search, symbol, context); } void CppFindReferences::renameUsages(Symbol *symbol, const LookupContext &context, const QString &replacement) { if (const Identifier *id = symbol->identifier()) { const QString textToReplace = replacement.isEmpty() ? QString::fromUtf8(id->chars(), id->size()) : replacement; findUsages(symbol, context, textToReplace, true); } } void CppFindReferences::findAll_helper(SearchResult *search, Symbol *symbol, const LookupContext &context) { if (!(symbol && symbol->identifier())) { search->finishSearch(false); return; } connect(search, &SearchResult::activated, this, &CppFindReferences::openEditor); SearchResultWindow::instance()->popup(IOutputPane::ModeSwitch | IOutputPane::WithFocus); const WorkingCopy workingCopy = m_modelManager->workingCopy(); QFuture result; result = Utils::runAsync(m_modelManager->sharedThreadPool(), find_helper, workingCopy, context, symbol); createWatcher(result, search); FutureProgress *progress = ProgressManager::addTask(result, tr("Searching for Usages"), CppTools::Constants::TASK_SEARCH); connect(progress, &FutureProgress::clicked, search, &SearchResult::popup); } static bool isAllLowerCase(const QString &text) { return text.toLower() == text; } void CppFindReferences::onReplaceButtonClicked(const QString &text, const QList &items, bool preserveCase) { const QStringList fileNames = TextEditor::BaseFileFind::replaceAll(text, items, preserveCase); if (!fileNames.isEmpty()) { m_modelManager->updateSourceFiles(fileNames.toSet()); SearchResultWindow::instance()->hide(); } auto search = qobject_cast(sender()); QTC_ASSERT(search, return); CppFindReferencesParameters parameters = search->userData().value(); if (parameters.filesToRename.isEmpty()) return; auto renameFilesCheckBox = qobject_cast(search->additionalReplaceWidget()); if (!renameFilesCheckBox || !renameFilesCheckBox->isChecked()) return; CppFileSettings settings; settings.fromSettings(Core::ICore::settings()); const QStringList newPaths = Utils::transform(parameters.filesToRename, [¶meters, text, &settings](const Node *node) -> QString { const QFileInfo fi = node->filePath().toFileInfo(); const QString oldSymbolName = parameters.prettySymbolName; const QString oldBaseName = fi.baseName(); const QString newSymbolName = text; QString newBaseName = newSymbolName; // 1) new symbol lowercase: new base name lowercase if (isAllLowerCase(newSymbolName)) { newBaseName = newSymbolName; // 2) old base name mixed case: new base name is verbatim symbol name } else if (!isAllLowerCase(oldBaseName)) { newBaseName = newSymbolName; // 3) old base name lowercase, old symbol mixed case: new base name lowercase } else if (!isAllLowerCase(oldSymbolName)) { newBaseName = newSymbolName.toLower(); // 4) old base name lowercase, old symbol lowercase, new symbol mixed case: // use the preferences setting for new base name case } else if (settings.lowerCaseFiles) { newBaseName = newSymbolName.toLower(); } if (newBaseName == oldBaseName) return QString(); return fi.absolutePath() + "/" + newBaseName + '.' + fi.completeSuffix(); }); for (int i = 0; i < parameters.filesToRename.size(); ++i) { if (!newPaths.at(i).isEmpty()) { Node *node = parameters.filesToRename.at(i); ProjectExplorerPlugin::renameFile(node, newPaths.at(i)); } } } void CppFindReferences::searchAgain() { SearchResult *search = qobject_cast(sender()); CppFindReferencesParameters parameters = search->userData().value(); parameters.filesToRename.clear(); Snapshot snapshot = CppModelManager::instance()->snapshot(); search->restart(); LookupContext context; Symbol *symbol = findSymbol(parameters, snapshot, &context); if (!symbol) { search->finishSearch(false); return; } findAll_helper(search, symbol, context); } namespace { class UidSymbolFinder : public SymbolVisitor { public: UidSymbolFinder(const QList &uid) : m_uid(uid), m_index(0), m_result(0) { } Symbol *result() const { return m_result; } bool preVisit(Symbol *symbol) { if (m_result) return false; int index = m_index; if (symbol->asScope()) ++m_index; if (index >= m_uid.size()) return false; if (idForSymbol(symbol) != m_uid.at(index)) return false; if (index == m_uid.size() - 1) { // symbol found m_result = symbol; return false; } return true; } void postVisit(Symbol *symbol) { if (symbol->asScope()) --m_index; } private: QList m_uid; int m_index; Symbol *m_result; }; } Symbol *CppFindReferences::findSymbol(const CppFindReferencesParameters ¶meters, const Snapshot &snapshot, LookupContext *context) { QTC_ASSERT(context, return 0); QString symbolFile = QLatin1String(parameters.symbolFileName); if (!snapshot.contains(symbolFile)) return 0; Document::Ptr newSymbolDocument = snapshot.document(symbolFile); // document is not parsed and has no bindings yet, do it QByteArray source = getSource(Utils::FileName::fromString(newSymbolDocument->fileName()), m_modelManager->workingCopy()); Document::Ptr doc = snapshot.preprocessedDocument(source, newSymbolDocument->fileName()); doc->check(); // find matching symbol in new document and return the new parameters UidSymbolFinder finder(parameters.symbolId); finder.accept(doc->globalNamespace()); if (finder.result()) { *context = LookupContext(doc, snapshot); return finder.result(); } return 0; } static void displayResults(SearchResult *search, QFutureWatcher *watcher, int first, int last) { CppFindReferencesParameters parameters = search->userData().value(); for (int index = first; index != last; ++index) { Usage result = watcher->future().resultAt(index); search->addResult(result.path.toString(), result.line, result.lineText, result.col, result.len); if (parameters.prettySymbolName.isEmpty()) continue; if (Utils::contains(parameters.filesToRename, Utils::equal(&Node::filePath, result.path))) continue; Node *node = SessionManager::nodeForFile(result.path); if (!node) // Not part of any project continue; const QFileInfo fi = node->filePath().toFileInfo(); if (fi.baseName().compare(parameters.prettySymbolName, Qt::CaseInsensitive) == 0) parameters.filesToRename.append(node); } search->setUserData(qVariantFromValue(parameters)); } static void searchFinished(SearchResult *search, QFutureWatcher *watcher) { search->finishSearch(watcher->isCanceled()); CppFindReferencesParameters parameters = search->userData().value(); if (!parameters.filesToRename.isEmpty()) { const QStringList filesToRename = Utils::transform(parameters.filesToRename, [](const Node *node) { return node->filePath().toUserOutput(); }); auto renameCheckBox = qobject_cast(search->additionalReplaceWidget()); if (renameCheckBox) { renameCheckBox->setText(CppFindReferences::tr("Re&name %1 files").arg(filesToRename.size())); renameCheckBox->setToolTip(CppFindReferences::tr("Files:\n%1").arg(filesToRename.join('\n'))); renameCheckBox->setVisible(true); } } watcher->deleteLater(); } void CppFindReferences::openEditor(const SearchResultItem &item) { if (item.path.size() > 0) { EditorManager::openEditorAt(QDir::fromNativeSeparators(item.path.first()), item.mainRange.begin.line, item.mainRange.begin.column); } else { EditorManager::openEditor(QDir::fromNativeSeparators(item.text)); } } namespace { class FindMacroUsesInFile: public std::unary_function > { const WorkingCopy workingCopy; const Snapshot snapshot; const Macro ¯o; QFutureInterface *future; public: FindMacroUsesInFile(const WorkingCopy &workingCopy, const Snapshot snapshot, const Macro ¯o, QFutureInterface *future) : workingCopy(workingCopy), snapshot(snapshot), macro(macro), future(future) { } QList operator()(const Utils::FileName &fileName) { QList usages; Document::Ptr doc = snapshot.document(fileName); QByteArray source; restart_search: if (future->isPaused()) future->waitForResume(); if (future->isCanceled()) return usages; usages.clear(); foreach (const Document::MacroUse &use, doc->macroUses()) { const Macro &useMacro = use.macro(); if (useMacro.fileName() == macro.fileName()) { // Check if this is a match, but possibly against an outdated document. if (source.isEmpty()) source = getSource(fileName, workingCopy); if (macro.fileRevision() > useMacro.fileRevision()) { // yes, it is outdated, so re-preprocess and start from scratch for this file. doc = snapshot.preprocessedDocument(source, fileName); usages.clear(); goto restart_search; } if (macro.name() == useMacro.name()) { unsigned column; const QString &lineSource = matchingLine(use.bytesBegin(), source, &column); usages.append(Usage(fileName, lineSource, use.beginLine(), column, useMacro.nameToQString().size())); } } } if (future->isPaused()) future->waitForResume(); return usages; } static QString matchingLine(unsigned bytesOffsetOfUseStart, const QByteArray &utf8Source, unsigned *columnOfUseStart = 0) { int lineBegin = utf8Source.lastIndexOf('\n', bytesOffsetOfUseStart) + 1; int lineEnd = utf8Source.indexOf('\n', bytesOffsetOfUseStart); if (lineEnd == -1) lineEnd = utf8Source.length(); if (columnOfUseStart) { *columnOfUseStart = 0; const char *startOfUse = utf8Source.constData() + bytesOffsetOfUseStart; QTC_ASSERT(startOfUse < utf8Source.constData() + lineEnd, return QString()); const char *currentSourceByte = utf8Source.constData() + lineBegin; unsigned char yychar = *currentSourceByte; while (currentSourceByte != startOfUse) Lexer::yyinp_utf8(currentSourceByte, yychar, *columnOfUseStart); } const QByteArray matchingLine = utf8Source.mid(lineBegin, lineEnd - lineBegin); return QString::fromUtf8(matchingLine, matchingLine.size()); } }; } // end of anonymous namespace static void findMacroUses_helper(QFutureInterface &future, const WorkingCopy workingCopy, const Snapshot snapshot, const Macro macro) { const Utils::FileName sourceFile = Utils::FileName::fromString(macro.fileName()); Utils::FileNameList files{sourceFile}; files = Utils::filteredUnique(files + snapshot.filesDependingOn(sourceFile)); future.setProgressRange(0, files.size()); FindMacroUsesInFile process(workingCopy, snapshot, macro, &future); UpdateUI reduce(&future); // This thread waits for blockingMappedReduced to finish, so reduce the pool's used thread count // so the blockingMappedReduced can use one more thread, and increase it again afterwards. QThreadPool::globalInstance()->releaseThread(); QtConcurrent::blockingMappedReduced > (files, process, reduce); QThreadPool::globalInstance()->reserveThread(); future.setProgressValue(files.size()); } void CppFindReferences::findMacroUses(const Macro ¯o) { findMacroUses(macro, QString(), false); } void CppFindReferences::findMacroUses(const Macro ¯o, const QString &replacement, bool replace) { SearchResult *search = SearchResultWindow::instance()->startNewSearch( tr("C++ Macro Usages:"), QString(), macro.nameToQString(), replace ? SearchResultWindow::SearchAndReplace : SearchResultWindow::SearchOnly, SearchResultWindow::PreserveCaseDisabled, QLatin1String("CppEditor")); search->setTextToReplace(replacement); connect(search, &SearchResult::replaceButtonClicked, this, &CppFindReferences::onReplaceButtonClicked); SearchResultWindow::instance()->popup(IOutputPane::ModeSwitch | IOutputPane::WithFocus); connect(search, &SearchResult::activated, this, &CppFindReferences::openEditor); const Snapshot snapshot = m_modelManager->snapshot(); const WorkingCopy workingCopy = m_modelManager->workingCopy(); // add the macro definition itself { const QByteArray &source = getSource(Utils::FileName::fromString(macro.fileName()), workingCopy); unsigned column; const QString line = FindMacroUsesInFile::matchingLine(macro.bytesOffset(), source, &column); search->addResult(macro.fileName(), macro.line(), line, column, macro.nameToQString().length()); } QFuture result; result = Utils::runAsync(m_modelManager->sharedThreadPool(), findMacroUses_helper, workingCopy, snapshot, macro); createWatcher(result, search); FutureProgress *progress = ProgressManager::addTask(result, tr("Searching for Usages"), CppTools::Constants::TASK_SEARCH); connect(progress, &FutureProgress::clicked, search, &SearchResult::popup); } void CppFindReferences::renameMacroUses(const Macro ¯o, const QString &replacement) { const QString textToReplace = replacement.isEmpty() ? macro.nameToQString() : replacement; findMacroUses(macro, textToReplace, true); } void CppFindReferences::createWatcher(const QFuture &future, SearchResult *search) { QFutureWatcher *watcher = new QFutureWatcher(); // auto-delete: connect(watcher, &QFutureWatcherBase::finished, watcher, [search, watcher]() { searchFinished(search, watcher); }); connect(watcher, &QFutureWatcherBase::resultsReadyAt, search, [search, watcher](int first, int last) { displayResults(search, watcher, first, last); }); connect(watcher, &QFutureWatcherBase::finished, search, [search, watcher]() { search->finishSearch(watcher->isCanceled()); }); connect(search, &SearchResult::cancelled, watcher, [watcher]() { watcher->cancel(); }); connect(search, &SearchResult::paused, watcher, [watcher](bool paused) { if (!paused || watcher->isRunning()) // guard against pausing when the search is finished watcher->setPaused(paused); }); watcher->setPendingResultsLimit(1); watcher->setFuture(future); }