CMake: Delay initialization of CMakeTools

Do not run cmake 5 times during startup. Delay that as far as possible.

Also add a supportedGenerators() method while visiting the code anyway.

Fix up and simplify the other cmake help output parsers.

Change-Id: I6622d552ffe559bf099b4b278618676a045e350e
Reviewed-by: Tobias Hunger <tobias.hunger@theqtcompany.com>
This commit is contained in:
Tobias Hunger
2016-01-22 13:50:58 +01:00
parent acdfb09687
commit fa17e27b7f
5 changed files with 210 additions and 266 deletions

View File

@@ -46,8 +46,6 @@ using namespace ProjectExplorer;
// -------------------------------
// CMakeFileCompletionAssistProvider
// -------------------------------
CMakeFileCompletionAssistProvider::CMakeFileCompletionAssistProvider()
{}
bool CMakeFileCompletionAssistProvider::supportsEditor(Core::Id editorId) const
{
@@ -56,31 +54,26 @@ bool CMakeFileCompletionAssistProvider::supportsEditor(Core::Id editorId) const
IAssistProcessor *CMakeFileCompletionAssistProvider::createProcessor() const
{
return new CMakeFileCompletionAssist();
return new CMakeFileCompletionAssist;
}
CMakeFileCompletionAssist::CMakeFileCompletionAssist() :
KeywordsCompletionAssistProcessor(Keywords())
{}
IAssistProposal *CMakeFileCompletionAssist::perform(const AssistInterface *interface)
{
TextEditor::Keywords keywords;
Keywords kw;
QString fileName = interface->fileName();
if (!fileName.isEmpty() && QFileInfo(fileName).isFile()) {
Utils::FileName file = Utils::FileName::fromString(fileName);
if (Project *p = SessionManager::projectForFile(file)) {
if (Target *t = p->activeTarget()) {
if (CMakeTool *cmake = CMakeKitInformation::cmakeTool(t->kit())) {
if (cmake->isValid())
keywords = cmake->keywords();
}
}
Project *p = SessionManager::projectForFile(Utils::FileName::fromString(fileName));
if (p && p->activeTarget()) {
CMakeTool *cmake = CMakeKitInformation::cmakeTool(p->activeTarget()->kit());
if (cmake && cmake->isValid())
kw = cmake->keywords();
}
}
setKeywords(keywords);
setKeywords(kw);
return KeywordsCompletionAssistProcessor::perform(interface);
}

View File

@@ -47,8 +47,6 @@ class CMakeFileCompletionAssistProvider : public TextEditor::CompletionAssistPro
Q_OBJECT
public:
CMakeFileCompletionAssistProvider();
bool supportsEditor(Core::Id editorId) const override;
TextEditor::IAssistProcessor *createProcessor() const override;
};

View File

@@ -26,12 +26,16 @@
#include "cmaketool.h"
#include "cmaketoolmanager.h"
#include <utils/algorithm.h>
#include <utils/environment.h>
#include <utils/qtcassert.h>
#include <QProcess>
#include <QFileInfo>
#include <QProcess>
#include <QSet>
#include <QTextDocument>
#include <QUuid>
#include <QVariantMap>
using namespace CMakeProjectManager;
@@ -44,16 +48,12 @@ const char CMAKE_INFORMATION_AUTODETECTED[] = "AutoDetected";
// CMakeTool
///////////////////////////
CMakeTool::CMakeTool(Detection d, const Core::Id &id) :
m_isAutoDetected(d == AutoDetection),
m_id(id)
m_id(id), m_isAutoDetected(d == AutoDetection)
{
//make sure every CMakeTool has a valid ID
if (!m_id.isValid())
createId();
QTC_ASSERT(m_id.isValid(), m_id = Core::Id::fromString(QUuid::createUuid().toString()));
}
CMakeTool::CMakeTool(const QVariantMap &map, bool fromSdk) :
m_isAutoDetected(fromSdk)
CMakeTool::CMakeTool(const QVariantMap &map, bool fromSdk) : m_isAutoDetected(fromSdk)
{
m_id = Core::Id::fromSetting(map.value(QLatin1String(CMAKE_INFORMATION_ID)));
m_displayName = map.value(QLatin1String(CMAKE_INFORMATION_DISPLAYNAME)).toString();
@@ -62,106 +62,57 @@ CMakeTool::CMakeTool(const QVariantMap &map, bool fromSdk) :
if (!fromSdk)
m_isAutoDetected = map.value(QLatin1String(CMAKE_INFORMATION_AUTODETECTED), false).toBool();
setCMakeExecutable(Utils::FileName::fromUserInput(map.value(QLatin1String(CMAKE_INFORMATION_COMMAND)).toString()));
setCMakeExecutable(Utils::FileName::fromString(map.value(QLatin1String(CMAKE_INFORMATION_COMMAND)).toString()));
}
CMakeTool::~CMakeTool()
Core::Id CMakeTool::createId()
{
cancel();
}
void CMakeTool::cancel()
{
if (m_process) {
disconnect(m_process, static_cast<void (QProcess::*)(int)>(&QProcess::finished),
this, &CMakeTool::finished);
if (m_process->state() != QProcess::NotRunning)
m_process->kill();
m_process->waitForFinished();
delete m_process;
m_process = 0;
}
return Core::Id::fromString(QUuid::createUuid().toString());
}
void CMakeTool::setCMakeExecutable(const Utils::FileName &executable)
{
cancel();
m_process = new QProcess();
connect(m_process, static_cast<void (QProcess::*)(int)>(&QProcess::finished), this, &CMakeTool::finished);
if (m_executable == executable)
return;
m_didRun = false;
m_didAttemptToRun = false;
m_executable = executable;
QFileInfo fi = m_executable.toFileInfo();
if (fi.exists() && fi.isExecutable()) {
// Run it to find out more
m_state = CMakeTool::RunningBasic;
if (!startProcess(QStringList(QLatin1String("--help"))))
m_state = CMakeTool::Invalid;
} else {
m_state = CMakeTool::Invalid;
}
CMakeToolManager::notifyAboutUpdate(this);
}
void CMakeTool::finished(int exitCode)
{
if (exitCode) {
m_state = CMakeTool::Invalid;
return;
}
if (m_state == CMakeTool::RunningBasic) {
QByteArray response = m_process->readAll();
m_hasCodeBlocksMsvcGenerator = response.contains("CodeBlocks - NMake Makefiles");
m_hasCodeBlocksNinjaGenerator = response.contains("CodeBlocks - Ninja");
if (response.isEmpty()) {
m_state = CMakeTool::Invalid;
} else {
m_state = CMakeTool::RunningFunctionList;
if (!startProcess(QStringList(QLatin1String("--help-command-list"))))
finished(0); // should never happen, just continue
}
} else if (m_state == CMakeTool::RunningFunctionList) {
parseFunctionOutput(m_process->readAll());
m_state = CMakeTool::RunningFunctionDetails;
if (!startProcess(QStringList(QLatin1String("--help-commands"))))
finished(0); // should never happen, just continue
} else if (m_state == CMakeTool::RunningFunctionDetails) {
parseFunctionDetailsOutput(m_process->readAll());
m_state = CMakeTool::RunningPropertyList;
if (!startProcess(QStringList(QLatin1String("--help-property-list"))))
finished(0); // should never happen, just continue
} else if (m_state == CMakeTool::RunningPropertyList) {
parseVariableOutput(m_process->readAll());
m_state = CMakeTool::RunningVariableList;
if (!startProcess(QStringList(QLatin1String("--help-variable-list"))))
finished(0); // should never happen, just continue
} else if (m_state == CMakeTool::RunningVariableList) {
parseVariableOutput(m_process->readAll());
parseDone();
m_state = CMakeTool::RunningDone;
}
}
bool CMakeTool::isValid() const
{
if (m_state == CMakeTool::Invalid || !m_id.isValid())
if (!m_id.isValid())
return false;
if (m_state == CMakeTool::RunningBasic) {
if (!m_process->waitForFinished(10000)) {
return false;
}
}
return (m_state != CMakeTool::Invalid);
if (!m_didAttemptToRun)
supportedGenerators();
return m_didRun;
}
void CMakeTool::createId()
Utils::SynchronousProcessResponse CMakeTool::run(const QString &arg) const
{
QTC_ASSERT(!m_id.isValid(), return);
m_id = Core::Id::fromString(QUuid::createUuid().toString());
if (m_didAttemptToRun && !m_didRun) {
Utils::SynchronousProcessResponse response;
response.result = Utils::SynchronousProcessResponse::StartFailed;
return response;
}
Utils::SynchronousProcess cmake;
cmake.setTimeoutS(1);
cmake.setFlags(Utils::SynchronousProcess::UnixTerminalDisabled);
Utils::Environment env = Utils::Environment::systemEnvironment();
env.set(QLatin1String("LC_ALL"), QLatin1String("C"));
cmake.setProcessEnvironment(env.toProcessEnvironment());
cmake.setTimeOutMessageBoxEnabled(false);
Utils::SynchronousProcessResponse response = cmake.run(m_executable.toString(), QStringList() << arg);
m_didAttemptToRun = true;
m_didRun = (response.result == Utils::SynchronousProcessResponse::Finished);
return response;
}
QVariantMap CMakeTool::toMap() const
@@ -174,12 +125,6 @@ QVariantMap CMakeTool::toMap() const
return data;
}
bool CMakeTool::startProcess(const QStringList &args)
{
m_process->start(m_executable.toString(), args);
return m_process->waitForStarted(2000);
}
Utils::FileName CMakeTool::cmakeExecutable() const
{
return m_executable;
@@ -189,24 +134,70 @@ bool CMakeTool::hasCodeBlocksMsvcGenerator() const
{
if (!isValid())
return false;
return m_hasCodeBlocksMsvcGenerator;
return supportedGenerators().contains(QLatin1String("CodeBlocks - NMake Makefiles"));
}
bool CMakeTool::hasCodeBlocksNinjaGenerator() const
{
if (!isValid())
return false;
return m_hasCodeBlocksNinjaGenerator;
return supportedGenerators().contains(QLatin1String("CodeBlocks - Ninja"));
}
QStringList CMakeTool::supportedGenerators() const
{
if (m_generators.isEmpty()) {
Utils::SynchronousProcessResponse response = run(QLatin1String("--help"));
if (response.result == Utils::SynchronousProcessResponse::Finished) {
bool inGeneratorSection = false;
const QStringList lines = response.stdOut.split(QLatin1Char('\n'));
foreach (const QString &line, lines) {
if (line.isEmpty())
continue;
if (line == QLatin1String("Generators"))
inGeneratorSection = true;
if (!inGeneratorSection)
continue;
if (line.startsWith(QLatin1String(" "))) {
int pos = line.indexOf(QLatin1Char('='));
if (pos < 0)
pos = line.length();
if (pos >= 0) {
--pos;
while (pos > 2 && line.at(pos).isSpace())
--pos;
}
if (pos > 2)
m_generators.append(line.mid(2, pos - 1));
}
}
}
}
return m_generators;
}
TextEditor::Keywords CMakeTool::keywords()
{
while (m_state != RunningDone && m_state != CMakeTool::Invalid) {
m_process->waitForFinished();
}
if (m_functions.isEmpty()) {
Utils::SynchronousProcessResponse response;
response = run(QLatin1String("--help-command-list"));
if (response.result == Utils::SynchronousProcessResponse::Finished)
m_functions = response.stdOut.split(QLatin1Char('\n'));
if (m_state == CMakeTool::Invalid)
return TextEditor::Keywords(QStringList(), QStringList(), QMap<QString, QStringList>());
response = run(QLatin1String("--help-commands"));
if (response.result == Utils::SynchronousProcessResponse::Finished)
parseFunctionDetailsOutput(response.stdOut);
response = run(QLatin1String("--help-property-list"));
if (response.result == Utils::SynchronousProcessResponse::Finished)
m_variables = parseVariableOutput(response.stdOut);
response = run(QLatin1String("--help-variable-list"));
if (response.result == Utils::SynchronousProcessResponse::Finished) {
m_variables.append(parseVariableOutput(response.stdOut));
m_variables = Utils::filteredUnique(m_variables);
Utils::sort(m_variables);
}
}
return TextEditor::Keywords(m_variables, m_functions, m_functionArgs);
}
@@ -216,53 +207,6 @@ bool CMakeTool::isAutoDetected() const
return m_isAutoDetected;
}
static void extractKeywords(const QByteArray &input, QStringList *destination)
{
if (!destination)
return;
QString keyword;
int ignoreZone = 0;
for (int i = 0; i < input.count(); ++i) {
const QChar chr = QLatin1Char(input.at(i));
if (chr == QLatin1Char('{'))
++ignoreZone;
if (chr == QLatin1Char('}'))
--ignoreZone;
if (ignoreZone == 0) {
if ((chr.isLetterOrNumber() && chr.isUpper())
|| chr == QLatin1Char('_')) {
keyword += chr;
} else {
if (!keyword.isEmpty()) {
if (keyword.size() > 1)
*destination << keyword;
keyword.clear();
}
}
}
}
if (keyword.size() > 1)
*destination << keyword;
}
void CMakeTool::parseFunctionOutput(const QByteArray &output)
{
QList<QByteArray> cmakeFunctionsList = output.split('\n');
m_functions.clear();
if (!cmakeFunctionsList.isEmpty()) {
cmakeFunctionsList.removeFirst(); //remove version string
foreach (const QByteArray &function, cmakeFunctionsList)
m_functions << QString::fromLocal8Bit(function.trimmed());
}
}
QString CMakeTool::formatFunctionDetails(const QString &command, const QString &args)
{
return QString::fromLatin1("<table><tr><td><b>%1</b></td><td>%2</td></tr>")
.arg(command.toHtmlEscaped(), args.toHtmlEscaped());
}
QString CMakeTool::displayName() const
{
return m_displayName;
@@ -286,78 +230,91 @@ QString CMakeTool::mapAllPaths(ProjectExplorer::Kit *kit, const QString &in) con
return in;
}
void CMakeTool::parseFunctionDetailsOutput(const QByteArray &output)
static QStringList parseDefinition(const QString &definition)
{
QStringList cmakeFunctionsList = m_functions;
QList<QByteArray> cmakeCommandsHelp = output.split('\n');
for (int i = 0; i < cmakeCommandsHelp.count(); ++i) {
QByteArray lineTrimmed = cmakeCommandsHelp.at(i).trimmed();
if (cmakeFunctionsList.isEmpty())
break;
if (cmakeFunctionsList.first().toLatin1() == lineTrimmed) {
QStringList commandSyntaxes;
QString currentCommandSyntax;
QString currentCommand = cmakeFunctionsList.takeFirst();
++i;
for (; i < cmakeCommandsHelp.count(); ++i) {
lineTrimmed = cmakeCommandsHelp.at(i).trimmed();
QStringList result;
QString word;
bool ignoreWord = false;
QVector<QChar> braceStack;
if (!cmakeFunctionsList.isEmpty() && cmakeFunctionsList.first().toLatin1() == lineTrimmed) {
//start of next function in output
if (!currentCommandSyntax.isEmpty())
commandSyntaxes << currentCommandSyntax.append(QLatin1String("</table>"));
--i;
break;
foreach (const QChar &c, definition) {
if (c == QLatin1Char('[') || c == QLatin1Char('<') || c == QLatin1Char('(')) {
braceStack.append(c);
ignoreWord = false;
} else if (c == QLatin1Char(']') || c == QLatin1Char('>') || c == QLatin1Char(')')) {
if (braceStack.isEmpty() || braceStack.takeLast() == QLatin1Char('<'))
ignoreWord = true;
}
if (lineTrimmed.startsWith(currentCommand.toLatin1() + "(")) {
if (!currentCommandSyntax.isEmpty())
commandSyntaxes << currentCommandSyntax.append(QLatin1String("</table>"));
QByteArray argLine = lineTrimmed.mid(currentCommand.length());
extractKeywords(argLine, &m_variables);
currentCommandSyntax = formatFunctionDetails(currentCommand, QString::fromUtf8(argLine));
if (c == QLatin1Char(' ') || c == QLatin1Char('[') || c == QLatin1Char('<') || c == QLatin1Char('(')
|| c == QLatin1Char(']') || c == QLatin1Char('>') || c == QLatin1Char(')')) {
if (!ignoreWord && !word.isEmpty()) {
if (result.isEmpty() || Utils::allOf(word, [](const QChar &c) { return c.isUpper() || c == QLatin1Char('_'); }))
result.append(word);
}
word.clear();
ignoreWord = false;
} else {
if (!currentCommandSyntax.isEmpty()) {
if (lineTrimmed.isEmpty()) {
commandSyntaxes << currentCommandSyntax.append(QLatin1String("</table>"));
currentCommandSyntax.clear();
word.append(c);
}
}
return result;
}
void CMakeTool::parseFunctionDetailsOutput(const QString &output)
{
QSet<QString> functionSet;
functionSet.fromList(m_functions);
bool expectDefinition = false;
QString currentDefinition;
const QStringList lines = output.split(QLatin1Char('\n'));
for (int i = 0; i < lines.count(); ++i) {
const QString line = lines.at(i);
if (line == QLatin1String("::")) {
expectDefinition = true;
continue;
}
if (expectDefinition) {
if (!line.startsWith(QLatin1Char(' ')) && !line.isEmpty()) {
expectDefinition = false;
QStringList words = parseDefinition(currentDefinition);
if (!words.isEmpty()) {
const QString command = words.takeFirst();
if (functionSet.contains(command)) {
QStringList tmp = words + m_functionArgs[command];
Utils::sort(tmp);
m_functionArgs[command] = Utils::filteredUnique(tmp);
}
}
if (!words.isEmpty() && functionSet.contains(words.at(0)))
m_functionArgs[words.at(0)];
currentDefinition.clear();
} else {
extractKeywords(lineTrimmed, &m_variables);
currentCommandSyntax += QString::fromLatin1("<tr><td>&nbsp;</td><td>%1</td></tr>")
.arg(QString::fromLocal8Bit(lineTrimmed).toHtmlEscaped());
}
}
}
}
m_functionArgs[currentCommand] = commandSyntaxes;
}
}
m_functions = m_functionArgs.keys();
}
void CMakeTool::parseVariableOutput(const QByteArray &output)
{
QList<QByteArray> variableList = output.split('\n');
if (!variableList.isEmpty()) {
variableList.removeFirst(); //remove version string
foreach (const QByteArray &variable, variableList) {
if (variable.contains("_<CONFIG>")) {
m_variables << QString::fromLocal8Bit(variable).replace(QLatin1String("_<CONFIG>"), QLatin1String("_DEBUG"));
m_variables << QString::fromLocal8Bit(variable).replace(QLatin1String("_<CONFIG>"), QLatin1String("_RELEASE"));
m_variables << QString::fromLocal8Bit(variable).replace(QLatin1String("_<CONFIG>"), QLatin1String("_MINSIZEREL"));
m_variables << QString::fromLocal8Bit(variable).replace(QLatin1String("_<CONFIG>"), QLatin1String("_RELWITHDEBINFO"));
} else if (variable.contains("_<LANG>")) {
m_variables << QString::fromLocal8Bit(variable).replace(QLatin1String("_<LANG>"), QLatin1String("_C"));
m_variables << QString::fromLocal8Bit(variable).replace(QLatin1String("_<LANG>"), QLatin1String("_CXX"));
} else if (!variable.contains("_<") && !variable.contains('[')) {
m_variables << QString::fromLocal8Bit(variable);
currentDefinition.append(line.trimmed() + QLatin1Char(' '));
}
}
}
}
void CMakeTool::parseDone()
QStringList CMakeTool::parseVariableOutput(const QString &output)
{
m_variables.sort();
m_variables.removeDuplicates();
const QStringList variableList = output.split(QLatin1Char('\n'));
QStringList result;
foreach (const QString &v, variableList) {
if (v.contains(QLatin1String("<CONFIG>"))) {
const QString tmp = QString(v).replace(QLatin1String("<CONFIG>"), QLatin1String("%1"));
result << tmp.arg(QLatin1String("DEBUG")) << tmp.arg(QLatin1String("RELEASE"))
<< tmp.arg(QLatin1String("MINSIZEREL")) << tmp.arg(QLatin1String("RELWITHDEBINFO"));
} else if (v.contains(QLatin1String("<LANG>"))) {
const QString tmp = QString(v).replace(QLatin1String("<LANG>"), QLatin1String("%1"));
result << tmp.arg(QLatin1String("C")) << tmp.arg(QLatin1String("CXX"));
} else if (!v.contains(QLatin1Char('<')) && !v.contains(QLatin1Char('['))) {
result << v;
}
}
return result;
}

View File

@@ -27,12 +27,14 @@
#include "cmake_global.h"
#include <texteditor/codeassist/keywordscompletionassist.h>
#include <utils/fileutils.h>
#include <coreplugin/id.h>
#include <texteditor/codeassist/keywordscompletionassist.h>
#include <utils/fileutils.h>
#include <utils/synchronousprocess.h>
#include <QObject>
#include <QString>
#include <QMap>
#include <QStringList>
QT_FORWARD_DECLARE_CLASS(QProcess)
@@ -52,23 +54,25 @@ public:
typedef std::function<QString (ProjectExplorer::Kit *, const QString &)> PathMapper;
explicit CMakeTool(Detection d, const Core::Id &id = Core::Id());
explicit CMakeTool(Detection d, const Core::Id &id);
explicit CMakeTool(const QVariantMap &map, bool fromSdk);
~CMakeTool() override;
~CMakeTool() override = default;
static Core::Id createId();
enum State { Invalid, RunningBasic, RunningFunctionList, RunningFunctionDetails,
RunningPropertyList, RunningVariableList, RunningDone };
void cancel();
bool isValid() const;
Core::Id id() const { return m_id; }
QVariantMap toMap () const;
void setCMakeExecutable(const Utils::FileName &executable);
Utils::FileName cmakeExecutable() const;
bool hasCodeBlocksMsvcGenerator() const;
bool hasCodeBlocksNinjaGenerator() const;
QStringList supportedGenerators() const;
TextEditor::Keywords keywords();
bool isAutoDetected() const;
QString displayName() const;
void setDisplayName(const QString &displayName);
@@ -77,32 +81,24 @@ public:
QString mapAllPaths(ProjectExplorer::Kit *kit, const QString &in) const;
private:
void finished(int exitCode);
void createId();
void finishStep();
void startNextStep();
bool startProcess(const QStringList &args);
void parseFunctionOutput(const QByteArray &output);
void parseFunctionDetailsOutput(const QByteArray &output);
void parseVariableOutput(const QByteArray &output);
void parseDone();
QString formatFunctionDetails(const QString &command, const QString &args);
State m_state = Invalid;
QProcess *m_process = 0;
Utils::FileName m_executable;
bool m_isAutoDetected;
bool m_hasCodeBlocksMsvcGenerator = false;
bool m_hasCodeBlocksNinjaGenerator = false;
QMap<QString, QStringList> m_functionArgs;
QStringList m_variables;
QStringList m_functions;
Utils::SynchronousProcessResponse run(const QString &arg) const;
void parseFunctionDetailsOutput(const QString &output);
QStringList parseVariableOutput(const QString &output);
Core::Id m_id;
QString m_displayName;
Utils::FileName m_executable;
bool m_isAutoDetected;
mutable bool m_didAttemptToRun;
mutable bool m_didRun;
mutable QStringList m_generators;
mutable QMap<QString, QStringList> m_functionArgs;
mutable QStringList m_variables;
mutable QStringList m_functions;
PathMapper m_pathMapper;
};

View File

@@ -131,7 +131,7 @@ static void readAndDeleteLegacyCMakeSettings ()
if (exec.toFileInfo().isExecutable()) {
CMakeTool *item = CMakeToolManager::findByCommand(exec);
if (!item) {
item = new CMakeTool(CMakeTool::ManualDetection);
item = new CMakeTool(CMakeTool::ManualDetection, CMakeTool::createId());
item->setCMakeExecutable(exec);
item->setDisplayName(CMakeToolManager::tr("CMake at %1").arg(item->cmakeExecutable().toUserOutput()));
@@ -182,7 +182,7 @@ static QList<CMakeTool *> autoDetectCMakeTools()
QList<CMakeTool *> found;
foreach (const FileName &command, suspects) {
auto item = new CMakeTool(CMakeTool::AutoDetection);
auto item = new CMakeTool(CMakeTool::AutoDetection, CMakeTool::createId());
item->setCMakeExecutable(command);
item->setDisplayName(CMakeToolManager::tr("System CMake at %1").arg(command.toUserOutput()));
@@ -247,7 +247,7 @@ Id CMakeToolManager::registerOrFindCMakeTool(const FileName &command)
if (cmake)
return cmake->id();
cmake = new CMakeTool(CMakeTool::ManualDetection);
cmake = new CMakeTool(CMakeTool::ManualDetection, CMakeTool::createId());
cmake->setCMakeExecutable(command);
cmake->setDisplayName(tr("CMake at %1").arg(command.toUserOutput()));