diff options
-rw-r--r-- | src/plugins/clangcodemodel/clangautomationutils.cpp | 140 | ||||
-rw-r--r-- | src/plugins/clangcodemodel/clangautomationutils.h | 46 | ||||
-rw-r--r-- | src/plugins/clangcodemodel/clangbatchfileprocessor.cpp | 799 | ||||
-rw-r--r-- | src/plugins/clangcodemodel/clangbatchfileprocessor.h | 36 | ||||
-rw-r--r-- | src/plugins/clangcodemodel/clangcodemodel.pro | 4 | ||||
-rw-r--r-- | src/plugins/clangcodemodel/clangcodemodel.qbs | 4 | ||||
-rw-r--r-- | src/plugins/clangcodemodel/clangcodemodelplugin.cpp | 20 | ||||
-rw-r--r-- | src/plugins/clangcodemodel/clangcodemodelplugin.h | 3 | ||||
-rw-r--r-- | src/plugins/clangcodemodel/test/clangcodecompletion_test.cpp | 99 |
9 files changed, 1052 insertions, 99 deletions
diff --git a/src/plugins/clangcodemodel/clangautomationutils.cpp b/src/plugins/clangcodemodel/clangautomationutils.cpp new file mode 100644 index 0000000000..29dc1c7155 --- /dev/null +++ b/src/plugins/clangcodemodel/clangautomationutils.cpp @@ -0,0 +1,140 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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 "clangautomationutils.h" + +#include "clangcompletionassistinterface.h" +#include "clangcompletionassistprovider.h" + +#include <texteditor/codeassist/assistinterface.h> +#include <texteditor/codeassist/assistproposalitem.h> +#include <texteditor/codeassist/completionassistprovider.h> +#include <texteditor/codeassist/genericproposalmodel.h> +#include <texteditor/codeassist/iassistprocessor.h> +#include <texteditor/codeassist/iassistproposal.h> +#include <texteditor/textdocument.h> +#include <texteditor/texteditor.h> + +#include <utils/qtcassert.h> + +#include <QCoreApplication> +#include <QElapsedTimer> + +namespace ClangCodeModel { +namespace Internal { + +class WaitForAsyncCompletions +{ +public: + enum WaitResult { GotResults, GotInvalidResults, Timeout }; + + WaitResult wait(TextEditor::IAssistProcessor *processor, + TextEditor::AssistInterface *assistInterface, + int timeoutInMs) + { + QTC_ASSERT(processor, return Timeout); + QTC_ASSERT(assistInterface, return Timeout); + + bool gotResults = false; + + processor->setAsyncCompletionAvailableHandler( + [this, &gotResults] (TextEditor::IAssistProposal *proposal) { + QTC_ASSERT(proposal, return); + proposalModel = proposal->model(); + delete proposal; + gotResults = true; + }); + + // Are there any immediate results? + if (TextEditor::IAssistProposal *proposal = processor->perform(assistInterface)) { + delete processor; + proposalModel = proposal->model(); + delete proposal; + QTC_ASSERT(proposalModel, return GotInvalidResults); + return GotResults; + } + + // There are not any, so wait for async results. + QElapsedTimer timer; + timer.start(); + while (!gotResults) { + if (timer.elapsed() >= timeoutInMs) + return Timeout; + QCoreApplication::processEvents(); + } + + return proposalModel ? GotResults : GotInvalidResults; + } + +public: + TextEditor::IAssistProposalModel *proposalModel; +}; + +static const CppTools::ProjectPartHeaderPaths toHeaderPaths(const QStringList &paths) +{ + using namespace CppTools; + + ProjectPartHeaderPaths result; + foreach (const QString &path, paths) + result << ProjectPartHeaderPath(path, ProjectPartHeaderPath::IncludePath); + return result; +} + +ProposalModel completionResults(TextEditor::BaseTextEditor *textEditor, + const QStringList &includePaths, + int timeOutInMs) +{ + using namespace TextEditor; + + TextEditorWidget *textEditorWidget = qobject_cast<TextEditorWidget *>(textEditor->widget()); + QTC_ASSERT(textEditorWidget, return ProposalModel()); + AssistInterface *assistInterface = textEditorWidget->createAssistInterface( + TextEditor::Completion, TextEditor::ExplicitlyInvoked); + QTC_ASSERT(assistInterface, return ProposalModel()); + if (!includePaths.isEmpty()) { + auto clangAssistInterface = static_cast<ClangCompletionAssistInterface *>(assistInterface); + clangAssistInterface->setHeaderPaths(toHeaderPaths(includePaths)); + } + + CompletionAssistProvider *assistProvider + = textEditor->textDocument()->completionAssistProvider(); + QTC_ASSERT(qobject_cast<ClangCompletionAssistProvider *>(assistProvider), + return ProposalModel()); + QTC_ASSERT(assistProvider, return ProposalModel()); + QTC_ASSERT(assistProvider->runType() == IAssistProvider::Asynchronous, return ProposalModel()); + + IAssistProcessor *processor = assistProvider->createProcessor(); + QTC_ASSERT(processor, return ProposalModel()); + + WaitForAsyncCompletions waitForCompletions; + const WaitForAsyncCompletions::WaitResult result = waitForCompletions.wait(processor, + assistInterface, + timeOutInMs); + QTC_ASSERT(result == WaitForAsyncCompletions::GotResults, return ProposalModel()); + return QSharedPointer<TextEditor::IAssistProposalModel>(waitForCompletions.proposalModel); +} + +} // namespace Internal +} // namespace ClangCodeModel diff --git a/src/plugins/clangcodemodel/clangautomationutils.h b/src/plugins/clangcodemodel/clangautomationutils.h new file mode 100644 index 0000000000..9942a597fd --- /dev/null +++ b/src/plugins/clangcodemodel/clangautomationutils.h @@ -0,0 +1,46 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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. +** +****************************************************************************/ + +#pragma once + +#include <QString> +#include <QSharedPointer> + +namespace TextEditor { +class BaseTextEditor; +class IAssistProposalModel; +} + +namespace ClangCodeModel { +namespace Internal { + +using ProposalModel = QSharedPointer<TextEditor::IAssistProposalModel>; + +ProposalModel completionResults(TextEditor::BaseTextEditor *textEditor, + const QStringList &includePaths = QStringList(), + int timeOutInMs = 10000); + +} // namespace Internal +} // namespace ClangCodeModel diff --git a/src/plugins/clangcodemodel/clangbatchfileprocessor.cpp b/src/plugins/clangcodemodel/clangbatchfileprocessor.cpp new file mode 100644 index 0000000000..c60aeda650 --- /dev/null +++ b/src/plugins/clangcodemodel/clangbatchfileprocessor.cpp @@ -0,0 +1,799 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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 "clangbatchfileprocessor.h" + +#include "clangautomationutils.h" + +#include <clangcodemodel/clangeditordocumentprocessor.h> + +#include <coreplugin/editormanager/editormanager.h> +#include <coreplugin/editormanager/ieditor.h> +#include <coreplugin/icore.h> +#include <cpptools/cpptoolsreuse.h> +#include <cpptools/cpptoolstestcase.h> +#include <cpptools/modelmanagertesthelper.h> +#include <cpptools/projectinfo.h> +#include <projectexplorer/projectexplorer.h> +#include <texteditor/codeassist/assistinterface.h> +#include <texteditor/codeassist/assistproposalitem.h> +#include <texteditor/codeassist/completionassistprovider.h> +#include <texteditor/codeassist/genericproposalmodel.h> +#include <texteditor/codeassist/iassistprocessor.h> +#include <texteditor/codeassist/iassistproposal.h> +#include <texteditor/textdocument.h> +#include <texteditor/texteditor.h> + +#include <utils/executeondestruction.h> +#include <utils/qtcassert.h> + +#include <QDebug> +#include <QFileInfo> +#include <QLoggingCategory> +#include <QSharedPointer> +#include <QString> +#include <QtTest> + +using namespace ClangBackEnd; +using namespace ClangCodeModel; +using namespace ClangCodeModel::Internal; +using namespace ProjectExplorer; + +static Q_LOGGING_CATEGORY(debug, "qtc.clangcodemodel.batch"); + +static int timeOutFromEnvironmentVariable() +{ + const QByteArray timeoutAsByteArray = qgetenv("QTC_CLANG_BATCH_TIMEOUT"); + + bool isConversionOk = false; + const int intervalAsInt = timeoutAsByteArray.toInt(&isConversionOk); + if (!isConversionOk) { + qCDebug(debug, "Environment variable QTC_CLANG_BATCH_TIMEOUT is not set, assuming 30000."); + return 30000; + } + + return intervalAsInt; +} + +static int timeOutInMs() +{ + static int timeOut = timeOutFromEnvironmentVariable(); + return timeOut; +} + +namespace { + +class BatchFileLineTokenizer +{ +public: + BatchFileLineTokenizer(const QString &line); + + QString nextToken(); + +private: + const QChar *advanceToTokenBegin(); + const QChar *advanceToTokenEnd(); + + bool atEnd() const; + bool atWhiteSpace() const; + bool atQuotationMark() const; + +private: + bool m_isWithinQuotation = false; + QString m_line; + const QChar *m_currentChar; +}; + +BatchFileLineTokenizer::BatchFileLineTokenizer(const QString &line) + : m_line(line) + , m_currentChar(m_line.unicode()) +{ +} + +QString BatchFileLineTokenizer::nextToken() +{ + if (const QChar *tokenBegin = advanceToTokenBegin()) { + if (const QChar *tokenEnd = advanceToTokenEnd()) { + const int length = tokenEnd - tokenBegin; + return QString(tokenBegin, length); + } + } + + return QString(); +} + +const QChar *BatchFileLineTokenizer::advanceToTokenBegin() +{ + m_isWithinQuotation = false; + + forever { + if (atEnd()) + return 0; + + if (atQuotationMark()) { + m_isWithinQuotation = true; + ++m_currentChar; + return m_currentChar; + } + + if (!atWhiteSpace()) + return m_currentChar; + + ++m_currentChar; + } +} + +const QChar *BatchFileLineTokenizer::advanceToTokenEnd() +{ + forever { + if (m_isWithinQuotation) { + if (atEnd()) { + qWarning("ClangBatchFileProcessor: error: unfinished quotation."); + return 0; + } + + if (atQuotationMark()) + return m_currentChar++; + + } else if (atWhiteSpace() || atEnd()) { + return m_currentChar; + } + + ++m_currentChar; + } +} + +bool BatchFileLineTokenizer::atEnd() const +{ + return *m_currentChar == QLatin1Char('\0'); +} + +bool BatchFileLineTokenizer::atWhiteSpace() const +{ + return *m_currentChar == ' ' + || *m_currentChar == '\t' + || *m_currentChar == '\n'; +} + +bool BatchFileLineTokenizer::atQuotationMark() const +{ + return *m_currentChar == '"'; +} + +struct CommandContext { + QString filePath; + int lineNumber = -1; +}; + +class Command +{ +public: + using Ptr = QSharedPointer<Command>; + +public: + Command(const CommandContext &context) : m_commandContext(context) {} + virtual ~Command() {} + + const CommandContext &context() const { return m_commandContext; } + virtual bool run() { return true; } + +private: + const CommandContext m_commandContext; +}; + +class OpenProjectCommand : public Command +{ +public: + OpenProjectCommand(const CommandContext &context, + const QString &projectFilePath); + + bool run() override; + + static Command::Ptr parse(BatchFileLineTokenizer &arguments, + const CommandContext &context); + +private: + QString m_projectFilePath; +}; + +OpenProjectCommand::OpenProjectCommand(const CommandContext &context, + const QString &projectFilePath) + : Command(context) + , m_projectFilePath(projectFilePath) +{ +} + +bool OpenProjectCommand::run() +{ + qCDebug(debug) << "line" << context().lineNumber << "OpenProjectCommand" << m_projectFilePath; + + const ProjectExplorerPlugin::OpenProjectResult openProjectSucceeded + = ProjectExplorerPlugin::openProject(m_projectFilePath); + QTC_ASSERT(openProjectSucceeded, return false); + + Project *project = openProjectSucceeded.project(); + project->configureAsExampleProject({}); + + return CppTools::Tests::TestCase::waitUntilCppModelManagerIsAwareOf(project, timeOutInMs()); +} + +Command::Ptr OpenProjectCommand::parse(BatchFileLineTokenizer &arguments, + const CommandContext &context) +{ + const QString projectFilePath = arguments.nextToken(); + if (projectFilePath.isEmpty()) { + qWarning("%s:%d: error: No project file path given.", + qPrintable(context.filePath), + context.lineNumber); + return Command::Ptr(); + } + + const QString absoluteProjectFilePath = QFileInfo(projectFilePath).absoluteFilePath(); + + return Command::Ptr(new OpenProjectCommand(context, absoluteProjectFilePath)); +} + +class OpenDocumentCommand : public Command +{ +public: + OpenDocumentCommand(const CommandContext &context, + const QString &documentFilePath); + + bool run() override; + + static Command::Ptr parse(BatchFileLineTokenizer &arguments, const CommandContext &context); + +private: + QString m_documentFilePath; +}; + +OpenDocumentCommand::OpenDocumentCommand(const CommandContext &context, + const QString &documentFilePath) + : Command(context) + , m_documentFilePath(documentFilePath) +{ +} + +class WaitForUpdatedCodeWarnings : public QObject +{ + Q_OBJECT + +public: + WaitForUpdatedCodeWarnings(ClangEditorDocumentProcessor *processor); + + bool wait(int timeOutInMs) const; + +private: + void onCodeWarningsUpdated() { m_gotResults = true; } + +private: + + bool m_gotResults = false; +}; + +WaitForUpdatedCodeWarnings::WaitForUpdatedCodeWarnings(ClangEditorDocumentProcessor *processor) +{ + connect(processor, + &ClangEditorDocumentProcessor::codeWarningsUpdated, + this, &WaitForUpdatedCodeWarnings::onCodeWarningsUpdated); +} + +bool WaitForUpdatedCodeWarnings::wait(int timeOutInMs) const +{ + QTime time; + time.start(); + + forever { + if (time.elapsed() > timeOutInMs) { + qWarning("WaitForUpdatedCodeWarnings: timeout of %d ms reached.", timeOutInMs); + return false; + } + + if (m_gotResults) + return true; + + QCoreApplication::processEvents(); + QThread::msleep(20); + } +} + +bool OpenDocumentCommand::run() +{ + qCDebug(debug) << "line" << context().lineNumber << "OpenDocumentCommand" << m_documentFilePath; + + const bool openEditorSucceeded = Core::EditorManager::openEditor(m_documentFilePath); + QTC_ASSERT(openEditorSucceeded, return false); + + auto *processor = ClangEditorDocumentProcessor::get(m_documentFilePath); + QTC_ASSERT(processor, return false); + + WaitForUpdatedCodeWarnings waiter(processor); + return waiter.wait(timeOutInMs()); +} + +Command::Ptr OpenDocumentCommand::parse(BatchFileLineTokenizer &arguments, + const CommandContext &context) +{ + const QString documentFilePath = arguments.nextToken(); + if (documentFilePath.isEmpty()) { + qWarning("%s:%d: error: No document file path given.", + qPrintable(context.filePath), + context.lineNumber); + return Command::Ptr(); + } + + const QString absoluteDocumentFilePath = QFileInfo(documentFilePath).absoluteFilePath(); + + return Command::Ptr(new OpenDocumentCommand(context, absoluteDocumentFilePath)); +} + +class CloseAllDocuments : public Command +{ +public: + CloseAllDocuments(const CommandContext &context); + + bool run() override; + + static Command::Ptr parse(BatchFileLineTokenizer &arguments, const CommandContext &context); +}; + +CloseAllDocuments::CloseAllDocuments(const CommandContext &context) + : Command(context) +{ +} + +bool CloseAllDocuments::run() +{ + qCDebug(debug) << "line" << context().lineNumber << "CloseAllDocuments"; + + return Core::EditorManager::closeAllEditors(/*askAboutModifiedEditors=*/ false); +} + +Command::Ptr CloseAllDocuments::parse(BatchFileLineTokenizer &arguments, + const CommandContext &context) +{ + const QString argument = arguments.nextToken(); + if (!argument.isEmpty()) { + qWarning("%s:%d: error: Unexpected argument.", + qPrintable(context.filePath), + context.lineNumber); + return Command::Ptr(); + } + + return Command::Ptr(new CloseAllDocuments(context)); +} + +class InsertTextCommand : public Command +{ +public: + // line and column are 1-based + InsertTextCommand(const CommandContext &context, const QString &text); + + bool run() override; + + static Command::Ptr parse(BatchFileLineTokenizer &arguments, + const CommandContext &context); + +private: + const QString m_textToInsert; +}; + +InsertTextCommand::InsertTextCommand(const CommandContext &context, const QString &text) + : Command(context) + , m_textToInsert(text) +{ +} + +TextEditor::BaseTextEditor *currentTextEditor() +{ + return qobject_cast<TextEditor::BaseTextEditor*>(Core::EditorManager::currentEditor()); +} + +bool InsertTextCommand::run() +{ + qCDebug(debug) << "line" << context().lineNumber << "InsertTextCommand" << m_textToInsert; + + TextEditor::BaseTextEditor *editor = currentTextEditor(); + QTC_ASSERT(editor, return false); + const QString documentFilePath = editor->document()->filePath().toString(); + auto *processor = ClangEditorDocumentProcessor::get(documentFilePath); + QTC_ASSERT(processor, return false); + + editor->insert(m_textToInsert); + + WaitForUpdatedCodeWarnings waiter(processor); + return waiter.wait(timeOutInMs()); +} + +Command::Ptr InsertTextCommand::parse(BatchFileLineTokenizer &arguments, + const CommandContext &context) +{ + const QString textToInsert = arguments.nextToken(); + if (textToInsert.isEmpty()) { + qWarning("%s:%d: error: No text to insert given.", + qPrintable(context.filePath), + context.lineNumber); + return Command::Ptr(); + } + + return Command::Ptr(new InsertTextCommand(context, textToInsert)); +} + +class CompleteCommand : public Command +{ +public: + CompleteCommand(const CommandContext &context); + + bool run() override; + + static Command::Ptr parse(BatchFileLineTokenizer &arguments, + const CommandContext &context); +}; + +CompleteCommand::CompleteCommand(const CommandContext &context) + : Command(context) +{ +} + +bool CompleteCommand::run() +{ + qCDebug(debug) << "line" << context().lineNumber << "CompleteCommand"; + + TextEditor::BaseTextEditor *editor = currentTextEditor(); + QTC_ASSERT(editor, return false); + + const QString documentFilePath = editor->document()->filePath().toString(); + auto *processor = ClangEditorDocumentProcessor::get(documentFilePath); + QTC_ASSERT(processor, return false); + + return completionResults(editor, QStringList(), timeOutInMs()); +} + +Command::Ptr CompleteCommand::parse(BatchFileLineTokenizer &arguments, + const CommandContext &context) +{ + Q_UNUSED(arguments) + Q_UNUSED(context) + + return Command::Ptr(new CompleteCommand(context)); +} + +class SetCursorCommand : public Command +{ +public: + // line and column are 1-based + SetCursorCommand(const CommandContext &context, int line, int column); + + bool run() override; + + static Command::Ptr parse(BatchFileLineTokenizer &arguments, + const CommandContext &context); + +private: + int m_line; + int m_column; +}; + +SetCursorCommand::SetCursorCommand(const CommandContext &context, int line, int column) + : Command(context) + , m_line(line) + , m_column(column) +{ +} + +bool SetCursorCommand::run() +{ + qCDebug(debug) << "line" << context().lineNumber << "SetCursorCommand" << m_line << m_column; + + TextEditor::BaseTextEditor *editor = currentTextEditor(); + QTC_ASSERT(editor, return false); + + editor->gotoLine(m_line, m_column - 1); + + return true; +} + +Command::Ptr SetCursorCommand::parse(BatchFileLineTokenizer &arguments, + const CommandContext &context) +{ + // Process line + const QString line = arguments.nextToken(); + if (line.isEmpty()) { + qWarning("%s:%d: error: No line number given.", + qPrintable(context.filePath), + context.lineNumber); + return Command::Ptr(); + } + bool converted = false; + const int lineNumber = line.toInt(&converted); + if (!converted) { + qWarning("%s:%d: error: Invalid line number.", + qPrintable(context.filePath), + context.lineNumber); + return Command::Ptr(); + } + + // Process column + const QString column = arguments.nextToken(); + if (column.isEmpty()) { + qWarning("%s:%d: error: No column number given.", + qPrintable(context.filePath), + context.lineNumber); + return Command::Ptr(); + } + converted = false; + const int columnNumber = column.toInt(&converted); + if (!converted) { + qWarning("%s:%d: error: Invalid column number.", + qPrintable(context.filePath), + context.lineNumber); + return Command::Ptr(); + } + + return Command::Ptr(new SetCursorCommand(context, lineNumber, columnNumber)); +} + +class ProcessEventsCommand : public Command +{ +public: + ProcessEventsCommand(const CommandContext &context, int durationInMs); + + bool run() override; + + static Command::Ptr parse(BatchFileLineTokenizer &arguments, + const CommandContext &context); + +private: + int m_durationInMs; +}; + +ProcessEventsCommand::ProcessEventsCommand(const CommandContext &context, + int durationInMs) + : Command(context) + , m_durationInMs(durationInMs) +{ +} + +bool ProcessEventsCommand::run() +{ + qCDebug(debug) << "line" << context().lineNumber << "ProcessEventsCommand" << m_durationInMs; + + QTime time; + time.start(); + + forever { + if (time.elapsed() > m_durationInMs) + return true; + + QCoreApplication::processEvents(); + QThread::msleep(20); + } +} + +Command::Ptr ProcessEventsCommand::parse(BatchFileLineTokenizer &arguments, + const CommandContext &context) +{ + const QString durationInMsText = arguments.nextToken(); + if (durationInMsText.isEmpty()) { + qWarning("%s:%d: error: No duration given.", + qPrintable(context.filePath), + context.lineNumber); + return Command::Ptr(); + } + + bool converted = false; + const int durationInMs = durationInMsText.toInt(&converted); + if (!converted) { + qWarning("%s:%d: error: Invalid duration given.", + qPrintable(context.filePath), + context.lineNumber); + return Command::Ptr(); + } + + return Command::Ptr(new ProcessEventsCommand(context, durationInMs)); +} + +class BatchFileReader +{ +public: + BatchFileReader(const QString &filePath); + + bool isFilePathValid() const; + + QString read() const; + +private: + const QString m_batchFilePath; +}; + +BatchFileReader::BatchFileReader(const QString &filePath) + : m_batchFilePath(filePath) +{ +} + +bool BatchFileReader::isFilePathValid() const +{ + QFileInfo fileInfo(m_batchFilePath); + + return !m_batchFilePath.isEmpty() + && fileInfo.isFile() + && fileInfo.isReadable(); +} + +QString BatchFileReader::read() const +{ + QFile file(m_batchFilePath); + QTC_CHECK(file.open(QFile::ReadOnly | QFile::Text)); + + return QString::fromLocal8Bit(file.readAll()); +} + +class BatchFileParser +{ +public: + BatchFileParser(const QString &filePath, + const QString &commands); + + bool parse(); + QVector<Command::Ptr> commands() const; + +private: + bool advanceLine(); + QString currentLine() const; + bool parseLine(const QString &line); + +private: + using ParseFunction = Command::Ptr (*)(BatchFileLineTokenizer &, const CommandContext &); + using CommandToParseFunction = QHash<QString, ParseFunction>; + CommandToParseFunction m_commandParsers; + + int m_currentLineIndex = -1; + CommandContext m_context; + QStringList m_lines; + QVector<Command::Ptr> m_commands; +}; + +BatchFileParser::BatchFileParser(const QString &filePath, + const QString &commands) + : m_lines(commands.split('\n')) +{ + m_context.filePath = filePath; + + m_commandParsers.insert("openProject", &OpenProjectCommand::parse); + m_commandParsers.insert("openDocument", &OpenDocumentCommand::parse); + m_commandParsers.insert("closeAllDocuments", &CloseAllDocuments::parse); + m_commandParsers.insert("setCursor", &SetCursorCommand::parse); + m_commandParsers.insert("insertText", &InsertTextCommand::parse); + m_commandParsers.insert("complete", &CompleteCommand::parse); + m_commandParsers.insert("processEvents", &ProcessEventsCommand::parse); +} + +bool BatchFileParser::parse() +{ + while (advanceLine()) { + const QString line = currentLine().trimmed(); + if (line.isEmpty() || line.startsWith('#')) + continue; + + if (!parseLine(line)) + return false; + } + + return true; +} + +QVector<Command::Ptr> BatchFileParser::commands() const +{ + return m_commands; +} + +bool BatchFileParser::advanceLine() +{ + ++m_currentLineIndex; + m_context.lineNumber = m_currentLineIndex + 1; + return m_currentLineIndex < m_lines.size(); +} + +QString BatchFileParser::currentLine() const +{ + return m_lines[m_currentLineIndex]; +} + +bool BatchFileParser::parseLine(const QString &line) +{ + BatchFileLineTokenizer tokenizer(line); + QString command = tokenizer.nextToken(); + QTC_CHECK(!command.isEmpty()); + + if (const ParseFunction parseFunction = m_commandParsers.value(command)) { + if (Command::Ptr cmd = parseFunction(tokenizer, m_context)) { + m_commands.append(cmd); + return true; + } + + return false; + } + + qWarning("%s:%d: error: Unknown command \"%s\".", + qPrintable(m_context.filePath), + m_context.lineNumber, + qPrintable(command)); + + return false; +} + +} // anonymous namespace + +namespace ClangCodeModel { +namespace Internal { + +static QString applySubstitutions(const QString &filePath, const QString &text) +{ + const QString dirPath = QFileInfo(filePath).absolutePath(); + + QString result = text; + result.replace("${PWD}", dirPath); + + return result; +} + +bool runClangBatchFile(const QString &filePath) +{ + qWarning("ClangBatchFileProcessor: Running \"%s\".", qPrintable(filePath)); + + BatchFileReader reader(filePath); + QTC_ASSERT(reader.isFilePathValid(), return false); + const QString fileContent = reader.read(); + const QString fileContentWithSubstitutionsApplied = applySubstitutions(filePath, fileContent); + + BatchFileParser parser(filePath, fileContentWithSubstitutionsApplied); + QTC_ASSERT(parser.parse(), return false); + const QVector<Command::Ptr> commands = parser.commands(); + + Utils::ExecuteOnDestruction closeAllEditors([](){ + qWarning("ClangBatchFileProcessor: Finished, closing all documents."); + QTC_CHECK(Core::EditorManager::closeAllEditors(/*askAboutModifiedEditors=*/ false)); + }); + + foreach (const Command::Ptr &command, commands) { + const bool runSucceeded = command->run(); + QCoreApplication::processEvents(); // Update GUI + + if (!runSucceeded) { + const CommandContext context = command->context(); + qWarning("%s:%d: Failed to run.", + qPrintable(context.filePath), + context.lineNumber); + return false; + } + } + + return true; +} + +} // namespace Internal +} // namespace ClangCodeModel + +#include "clangbatchfileprocessor.moc" diff --git a/src/plugins/clangcodemodel/clangbatchfileprocessor.h b/src/plugins/clangcodemodel/clangbatchfileprocessor.h new file mode 100644 index 0000000000..40f0bceee3 --- /dev/null +++ b/src/plugins/clangcodemodel/clangbatchfileprocessor.h @@ -0,0 +1,36 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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. +** +****************************************************************************/ + +#pragma once + +#include <QString> + +namespace ClangCodeModel { +namespace Internal { + +bool runClangBatchFile(const QString &filePath); + +} // namespace Internal +} // namespace ClangCodeModel diff --git a/src/plugins/clangcodemodel/clangcodemodel.pro b/src/plugins/clangcodemodel/clangcodemodel.pro index ef9302a1d1..3089483407 100644 --- a/src/plugins/clangcodemodel/clangcodemodel.pro +++ b/src/plugins/clangcodemodel/clangcodemodel.pro @@ -9,7 +9,9 @@ SOURCES += \ clangassistproposal.cpp \ clangassistproposalitem.cpp \ clangassistproposalmodel.cpp \ + clangautomationutils.cpp \ clangbackendipcintegration.cpp \ + clangbatchfileprocessor.cpp \ clangcodemodelplugin.cpp \ clangcompletionassistinterface.cpp \ clangcompletionassistprocessor.cpp \ @@ -39,7 +41,9 @@ HEADERS += \ clangassistproposal.h \ clangassistproposalitem.h \ clangassistproposalmodel.h \ + clangautomationutils.h \ clangbackendipcintegration.h \ + clangbatchfileprocessor.h \ clangcodemodelplugin.h \ clangcompletionassistinterface.h \ clangcompletionassistprocessor.h \ diff --git a/src/plugins/clangcodemodel/clangcodemodel.qbs b/src/plugins/clangcodemodel/clangcodemodel.qbs index 02ff530760..28e4dffcd8 100644 --- a/src/plugins/clangcodemodel/clangcodemodel.qbs +++ b/src/plugins/clangcodemodel/clangcodemodel.qbs @@ -41,8 +41,12 @@ QtcPlugin { "clangassistproposalitem.h", "clangassistproposalmodel.cpp", "clangassistproposalmodel.h", + "clangautomationutils.cpp", + "clangautomationutils.h", "clangbackendipcintegration.cpp", "clangbackendipcintegration.h", + "clangbatchfileprocessor.cpp", + "clangbatchfileprocessor.h", "clangcodemodel.qrc", "clangcodemodelplugin.cpp", "clangcodemodelplugin.h", diff --git a/src/plugins/clangcodemodel/clangcodemodelplugin.cpp b/src/plugins/clangcodemodel/clangcodemodelplugin.cpp index ba1bc10376..0aa9cbff3f 100644 --- a/src/plugins/clangcodemodel/clangcodemodelplugin.cpp +++ b/src/plugins/clangcodemodel/clangcodemodelplugin.cpp @@ -25,6 +25,7 @@ #include "clangcodemodelplugin.h" +#include "clangbatchfileprocessor.h" #include "clangconstants.h" #include "clangprojectsettingswidget.h" @@ -74,8 +75,13 @@ void addProjectPanelWidget() bool ClangCodeModelPlugin::initialize(const QStringList &arguments, QString *errorMessage) { - Q_UNUSED(arguments) - Q_UNUSED(errorMessage) + Q_UNUSED(arguments); + Q_UNUSED(errorMessage); + + connect(ProjectExplorer::ProjectExplorerPlugin::instance(), + &ProjectExplorer::ProjectExplorerPlugin::finishedInitialization, + this, + &ClangCodeModelPlugin::maybeHandleBatchFileAndExit); CppTools::CppModelManager::instance()->activateClangCodeModel(&m_modelManagerSupportProvider); @@ -89,6 +95,16 @@ void ClangCodeModelPlugin::extensionsInitialized() { } +// For e.g. creation of profile-guided optimization builds. +void ClangCodeModelPlugin::maybeHandleBatchFileAndExit() const +{ + const QString batchFilePath = QString::fromLocal8Bit(qgetenv("QTC_CLANG_BATCH")); + if (!batchFilePath.isEmpty() && QTC_GUARD(QFileInfo::exists(batchFilePath))) { + const bool runSucceeded = runClangBatchFile(batchFilePath); + QCoreApplication::exit(!runSucceeded); + } +} + #ifdef WITH_TESTS QList<QObject *> ClangCodeModelPlugin::createTestObjects() const { diff --git a/src/plugins/clangcodemodel/clangcodemodelplugin.h b/src/plugins/clangcodemodel/clangcodemodelplugin.h index 0b94c93c2c..8e41fe6020 100644 --- a/src/plugins/clangcodemodel/clangcodemodelplugin.h +++ b/src/plugins/clangcodemodel/clangcodemodelplugin.h @@ -42,6 +42,9 @@ public: void extensionsInitialized(); private: + void maybeHandleBatchFileAndExit() const; + +private: ModelManagerSupportProviderClang m_modelManagerSupportProvider; #ifdef WITH_TESTS diff --git a/src/plugins/clangcodemodel/test/clangcodecompletion_test.cpp b/src/plugins/clangcodemodel/test/clangcodecompletion_test.cpp index 0f9793dc15..f5072068f6 100644 --- a/src/plugins/clangcodemodel/test/clangcodecompletion_test.cpp +++ b/src/plugins/clangcodemodel/test/clangcodecompletion_test.cpp @@ -25,6 +25,7 @@ #include "clangcodecompletion_test.h" +#include "../clangautomationutils.h" #include "../clangbackendipcintegration.h" #include "../clangcompletionassistinterface.h" #include "../clangmodelmanagersupport.h" @@ -38,12 +39,8 @@ #include <cpptools/cpptoolstestcase.h> #include <cpptools/modelmanagertesthelper.h> #include <cpptools/projectinfo.h> -#include <texteditor/codeassist/assistinterface.h> #include <texteditor/codeassist/assistproposalitem.h> -#include <texteditor/codeassist/completionassistprovider.h> #include <texteditor/codeassist/genericproposalmodel.h> -#include <texteditor/codeassist/iassistprocessor.h> -#include <texteditor/codeassist/iassistproposal.h> #include <texteditor/textdocument.h> #include <texteditor/texteditor.h> @@ -180,53 +177,6 @@ void insertTextAtTopOfEditor(TextEditor::BaseTextEditor *editor, const QByteArra cs.apply(&textCursor); } -class WaitForAsyncCompletions -{ -public: - enum WaitResult { GotResults, GotInvalidResults, Timeout }; - WaitResult wait(TextEditor::IAssistProcessor *processor, - TextEditor::AssistInterface *assistInterface); - - TextEditor::IAssistProposalModel *proposalModel; -}; - -WaitForAsyncCompletions::WaitResult WaitForAsyncCompletions::wait( - TextEditor::IAssistProcessor *processor, - TextEditor::AssistInterface *assistInterface) -{ - QTC_ASSERT(processor, return Timeout); - QTC_ASSERT(assistInterface, return Timeout); - - bool gotResults = false; - - processor->setAsyncCompletionAvailableHandler( - [this, &gotResults] (TextEditor::IAssistProposal *proposal) { - QTC_ASSERT(proposal, return); - proposalModel = proposal->model(); - delete proposal; - gotResults = true; - }); - - // Are there any immediate results? - if (TextEditor::IAssistProposal *proposal = processor->perform(assistInterface)) { - delete processor; - proposalModel = proposal->model(); - delete proposal; - QTC_ASSERT(proposalModel, return GotInvalidResults); - return GotResults; - } - - // There are not any, so wait for async results. - QElapsedTimer timer; timer.start(); - while (!gotResults) { - if (timer.elapsed() >= 30 * 1000) - return Timeout; - QCoreApplication::processEvents(); - } - - return proposalModel ? GotResults : GotInvalidResults; -} - class ChangeDocumentReloadSetting { public: @@ -421,51 +371,6 @@ public: QString senderLog; }; -const CppTools::ProjectPartHeaderPaths toHeaderPaths(const QStringList &paths) -{ - using namespace CppTools; - - ProjectPartHeaderPaths result; - foreach (const QString &path, paths) - result << ProjectPartHeaderPath(path, ProjectPartHeaderPath::IncludePath); - return result; -} - -using ProposalModel = QSharedPointer<TextEditor::IAssistProposalModel>; - -ProposalModel completionResults( - TextEditor::BaseTextEditor *textEditor, - const QStringList &includePaths = QStringList()) -{ - using namespace TextEditor; - - TextEditorWidget *textEditorWidget = qobject_cast<TextEditorWidget *>(textEditor->widget()); - QTC_ASSERT(textEditorWidget, return ProposalModel()); - AssistInterface *assistInterface = textEditorWidget->createAssistInterface( - TextEditor::Completion, TextEditor::ExplicitlyInvoked); - QTC_ASSERT(assistInterface, return ProposalModel()); - if (!includePaths.isEmpty()) { - auto clangAssistInterface = static_cast<ClangCompletionAssistInterface *>(assistInterface); - clangAssistInterface->setHeaderPaths(toHeaderPaths(includePaths)); - } - - CompletionAssistProvider *assistProvider - = textEditor->textDocument()->completionAssistProvider(); - QTC_ASSERT(qobject_cast<ClangCompletionAssistProvider *>(assistProvider), - return ProposalModel()); - QTC_ASSERT(assistProvider, return ProposalModel()); - QTC_ASSERT(assistProvider->runType() == IAssistProvider::Asynchronous, return ProposalModel()); - - IAssistProcessor *processor = assistProvider->createProcessor(); - QTC_ASSERT(processor, return ProposalModel()); - - WaitForAsyncCompletions waitForCompletions; - const WaitForAsyncCompletions::WaitResult result = waitForCompletions.wait(processor, - assistInterface); - QTC_ASSERT(result == WaitForAsyncCompletions::GotResults, return ProposalModel()); - return QSharedPointer<TextEditor::IAssistProposalModel>(waitForCompletions.proposalModel); -} - class TestDocument { public: @@ -689,7 +594,7 @@ public: if (!textToInsert.isEmpty()) openEditor.editor()->insert(textToInsert); - proposal = completionResults(openEditor.editor(), includePaths); + proposal = completionResults(openEditor.editor(), includePaths, 15000); } ProposalModel proposal; |