forked from qt-creator/qt-creator
This change involves the relocation of SyntaxHighlighter processing to another thread. The core idea is to create a duplicate of the original TextDocument using SyntaxHighlighterRunnerPrivate::cloneDocument. A new SyntaxHighlighter is then instantiated by SyntaxHighLighterCreator for the cloned document. The entire SyntaxHighLighterCreator class is moved to a new thread, where it performs highlighting on the cloned document. Upon completion of the highlighting process, the resultsReady signal is emitted, and the updated highlighting data is applied to the original document. This shift of SyntaxHighlighter to another thread enhances the user experience by preventing UI slowdowns during the highlighting process. - Introduction of BaseSyntaxHighlighterRunner as an interface class for future *SyntaxHighlighterRunner. - Inclusion of DirectSyntaxHighlighterRunner class for performing highlighting in the main thread, suitable for syntax highlighters that cannot be moved to another thread. - Introduction of ThreadedSyntaxHighlighterRunner class for highlighting in a separate thread, preventing UI blocking during the process. - Addition of Result data to the SyntaxHighlighter class to facilitate data exchange between threads. Task-number: QTCREATORBUG-28727 Change-Id: I4b6a38d15f5ec9b8828055d38d2a0c6f21a657b4 Reviewed-by: Jarek Kobus <jaroslaw.kobus@qt.io> Reviewed-by: David Schulz <david.schulz@qt.io>
508 lines
16 KiB
C++
508 lines
16 KiB
C++
// Copyright (C) 2018 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
|
|
|
#include "clangformatconfigwidget.h"
|
|
|
|
// the file below was generated by scripts/generateClangFormatChecksLayout.py
|
|
#include "clangformatchecks.h"
|
|
#include "clangformatconstants.h"
|
|
#include "clangformatfile.h"
|
|
#include "clangformatindenter.h"
|
|
#include "clangformattr.h"
|
|
#include "clangformatutils.h"
|
|
|
|
#include <coreplugin/icore.h>
|
|
|
|
#include <cppeditor/cppcodestylepreferences.h>
|
|
#include <cppeditor/cppcodestylesettings.h>
|
|
#include <cppeditor/cppcodestylesnippets.h>
|
|
#include <cppeditor/cpphighlighter.h>
|
|
#include <cppeditor/cpptoolssettings.h>
|
|
|
|
#include <projectexplorer/editorconfiguration.h>
|
|
#include <projectexplorer/project.h>
|
|
|
|
#include <texteditor/displaysettings.h>
|
|
#include <texteditor/icodestylepreferences.h>
|
|
#include <texteditor/snippets/snippeteditor.h>
|
|
#include <texteditor/textdocument.h>
|
|
#include <texteditor/texteditorsettings.h>
|
|
|
|
#include <utils/guard.h>
|
|
#include <utils/layoutbuilder.h>
|
|
#include <utils/qtcassert.h>
|
|
#include <utils/utilsicons.h>
|
|
|
|
#include <QComboBox>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QPushButton>
|
|
#include <QScrollArea>
|
|
#include <QSharedPointer>
|
|
#include <QVBoxLayout>
|
|
#include <QVersionNumber>
|
|
#include <QWeakPointer>
|
|
#include <QWidget>
|
|
|
|
#include <clang/Basic/Version.h>
|
|
#include <clang/Format/Format.h>
|
|
|
|
#include <sstream>
|
|
|
|
using namespace ProjectExplorer;
|
|
using namespace Utils;
|
|
|
|
namespace ClangFormat {
|
|
|
|
class ClangFormatConfigWidget::Private
|
|
{
|
|
public:
|
|
ProjectExplorer::Project *project = nullptr;
|
|
QWidget *checksWidget = nullptr;
|
|
QScrollArea *checksScrollArea = nullptr;
|
|
TextEditor::SnippetEditorWidget *preview = nullptr;
|
|
std::unique_ptr<ClangFormatFile> config;
|
|
clang::format::FormatStyle style;
|
|
Utils::Guard ignoreChanges;
|
|
QLabel *fallbackConfig;
|
|
QLabel *clangVersion;
|
|
QLabel *clangWarningText;
|
|
QLabel *clangWarningIcon;
|
|
};
|
|
|
|
bool ClangFormatConfigWidget::eventFilter(QObject *object, QEvent *event)
|
|
{
|
|
if (event->type() == QEvent::Wheel && qobject_cast<QComboBox *>(object)) {
|
|
event->ignore();
|
|
return true;
|
|
}
|
|
return QWidget::eventFilter(object, event);
|
|
}
|
|
|
|
ClangFormatConfigWidget::ClangFormatConfigWidget(TextEditor::ICodeStylePreferences *codeStyle,
|
|
ProjectExplorer::Project *project,
|
|
QWidget *parent)
|
|
: CppCodeStyleWidget(parent), d(new Private)
|
|
{
|
|
d->project = project;
|
|
d->config = std::make_unique<ClangFormatFile>(codeStyle->currentPreferences());
|
|
|
|
d->fallbackConfig = new QLabel(Tr::tr("Clang-Format Style"));
|
|
d->checksScrollArea = new QScrollArea();
|
|
d->checksWidget = new ClangFormatChecks();
|
|
|
|
d->checksScrollArea->setWidget(d->checksWidget);
|
|
d->checksScrollArea->setWidgetResizable(true);
|
|
d->checksWidget->setEnabled(!codeStyle->isReadOnly() && !codeStyle->isTemporarilyReadOnly()
|
|
&& !codeStyle->isAdditionalTabDisabled());
|
|
|
|
|
|
static const int expectedMajorVersion = 17;
|
|
d->clangVersion = new QLabel(Tr::tr("Current ClangFormat version: %1.").arg(LLVM_VERSION_STRING),
|
|
this);
|
|
d->clangWarningText
|
|
= new QLabel(Tr::tr("The widget was generated for ClangFormat %1. "
|
|
"If you use a different version, the widget may work incorrectly.")
|
|
.arg(expectedMajorVersion),
|
|
this);
|
|
|
|
QPalette palette = d->clangWarningText->palette();
|
|
palette.setColor(QPalette::WindowText, Qt::red);
|
|
d->clangWarningText->setPalette(palette);
|
|
|
|
d->clangWarningIcon = new QLabel(this);
|
|
d->clangWarningIcon->setPixmap(Utils::Icons::WARNING.icon().pixmap(16, 16));
|
|
|
|
if (LLVM_VERSION_MAJOR == expectedMajorVersion) {
|
|
d->clangWarningText->hide();
|
|
d->clangWarningIcon->hide();
|
|
}
|
|
|
|
FilePath fileName;
|
|
if (d->project)
|
|
fileName = d->project->projectFilePath().pathAppended("snippet.cpp");
|
|
else
|
|
fileName = Core::ICore::userResourcePath("snippet.cpp");
|
|
|
|
d->preview = new TextEditor::SnippetEditorWidget(this);
|
|
TextEditor::DisplaySettings displaySettings = d->preview->displaySettings();
|
|
displaySettings.m_visualizeWhitespace = true;
|
|
d->preview->setDisplaySettings(displaySettings);
|
|
d->preview->setPlainText(QLatin1String(CppEditor::Constants::DEFAULT_CODE_STYLE_SNIPPETS[0]));
|
|
d->preview->textDocument()->setIndenter(new ClangFormatIndenter(d->preview->document()));
|
|
d->preview->textDocument()->setFontSettings(TextEditor::TextEditorSettings::fontSettings());
|
|
d->preview->textDocument()->resetSyntaxHighlighter(
|
|
[] { return new CppEditor::CppHighlighter(); });
|
|
d->preview->textDocument()->indenter()->setFileName(fileName);
|
|
|
|
using namespace Layouting;
|
|
|
|
Column {
|
|
d->fallbackConfig,
|
|
Row {d->clangWarningIcon, d->clangWarningText, st},
|
|
d->clangVersion,
|
|
Row { d->checksScrollArea, d->preview },
|
|
}.attachTo(this);
|
|
|
|
connect(codeStyle, &TextEditor::ICodeStylePreferences::currentPreferencesChanged,
|
|
this, &ClangFormatConfigWidget::slotCodeStyleChanged);
|
|
|
|
slotCodeStyleChanged(codeStyle->currentPreferences());
|
|
|
|
showOrHideWidgets();
|
|
fillTable();
|
|
updatePreview();
|
|
|
|
connectChecks();
|
|
}
|
|
|
|
ClangFormatConfigWidget::~ClangFormatConfigWidget()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
void ClangFormatConfigWidget::slotCodeStyleChanged(
|
|
TextEditor::ICodeStylePreferences *codeStyle)
|
|
{
|
|
if (!codeStyle)
|
|
return;
|
|
d->config.reset(new ClangFormatFile(codeStyle));
|
|
d->config->setIsReadOnly(codeStyle->isReadOnly());
|
|
d->style = d->config->style();
|
|
|
|
d->checksWidget->setEnabled(!codeStyle->isReadOnly() && !codeStyle->isTemporarilyReadOnly()
|
|
&& !codeStyle->isAdditionalTabDisabled());
|
|
|
|
fillTable();
|
|
updatePreview();
|
|
}
|
|
|
|
void ClangFormatConfigWidget::connectChecks()
|
|
{
|
|
auto doSaveChanges = [this](QObject *sender) {
|
|
if (!d->ignoreChanges.isLocked())
|
|
saveChanges(sender);
|
|
};
|
|
|
|
for (QObject *child : d->checksWidget->children()) {
|
|
auto comboBox = qobject_cast<QComboBox *>(child);
|
|
if (comboBox != nullptr) {
|
|
connect(comboBox, &QComboBox::currentIndexChanged,
|
|
this, std::bind(doSaveChanges, comboBox));
|
|
comboBox->installEventFilter(this);
|
|
continue;
|
|
}
|
|
|
|
const auto button = qobject_cast<QPushButton *>(child);
|
|
if (button != nullptr)
|
|
connect(button, &QPushButton::clicked, this, std::bind(doSaveChanges, button));
|
|
}
|
|
}
|
|
|
|
static clang::format::FormatStyle constructStyle(const QByteArray &baseStyle = QByteArray())
|
|
{
|
|
if (!baseStyle.isEmpty()) {
|
|
// Try to get the style for this base style.
|
|
llvm::Expected<clang::format::FormatStyle> style
|
|
= clang::format::getStyle(baseStyle.toStdString(), "dummy.cpp", baseStyle.toStdString());
|
|
if (style)
|
|
return *style;
|
|
|
|
handleAllErrors(style.takeError(), [](const llvm::ErrorInfoBase &) {
|
|
// do nothing
|
|
});
|
|
// Fallthrough to the default style.
|
|
}
|
|
return qtcStyle();
|
|
}
|
|
|
|
Utils::FilePath ClangFormatConfigWidget::globalPath()
|
|
{
|
|
return Core::ICore::userResourcePath();
|
|
}
|
|
|
|
Utils::FilePath ClangFormatConfigWidget::projectPath()
|
|
{
|
|
if (d->project)
|
|
return globalPath().pathAppended("clang-format/" + projectUniqueId(d->project));
|
|
|
|
return {};
|
|
}
|
|
|
|
void ClangFormatConfigWidget::createStyleFileIfNeeded(bool isGlobal)
|
|
{
|
|
const FilePath path = isGlobal ? globalPath() : projectPath();
|
|
const FilePath configFile = path / Constants::SETTINGS_FILE_NAME;
|
|
|
|
if (configFile.exists())
|
|
return;
|
|
|
|
path.ensureWritableDir();
|
|
if (!isGlobal) {
|
|
FilePath possibleProjectConfig = d->project->rootProjectDirectory()
|
|
/ Constants::SETTINGS_FILE_NAME;
|
|
if (possibleProjectConfig.exists()) {
|
|
// Just copy th .clang-format if current project has one.
|
|
possibleProjectConfig.copyFile(configFile);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const std::string config = clang::format::configurationAsText(constructStyle());
|
|
configFile.writeFileContents(QByteArray::fromStdString(config));
|
|
}
|
|
|
|
void ClangFormatConfigWidget::showOrHideWidgets()
|
|
{
|
|
auto verticalLayout = qobject_cast<QVBoxLayout *>(layout());
|
|
QTC_ASSERT(verticalLayout, return);
|
|
|
|
QLayoutItem *lastItem = verticalLayout->itemAt(verticalLayout->count() - 1);
|
|
if (lastItem->spacerItem())
|
|
verticalLayout->removeItem(lastItem);
|
|
|
|
createStyleFileIfNeeded(!d->project);
|
|
d->fallbackConfig->show();
|
|
d->checksScrollArea->show();
|
|
d->preview->show();
|
|
}
|
|
|
|
void ClangFormatConfigWidget::updatePreview()
|
|
{
|
|
QTextCursor cursor(d->preview->document());
|
|
cursor.setPosition(0);
|
|
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
|
d->preview->textDocument()->autoIndent(cursor);
|
|
}
|
|
|
|
std::string ClangFormatConfigWidget::readFile(const QString &path)
|
|
{
|
|
const std::string defaultStyle = clang::format::configurationAsText(qtcStyle());
|
|
|
|
QFile file(path);
|
|
if (!file.open(QFile::ReadOnly))
|
|
return defaultStyle;
|
|
|
|
const std::string content = file.readAll().toStdString();
|
|
file.close();
|
|
|
|
clang::format::FormatStyle style;
|
|
style.Language = clang::format::FormatStyle::LK_Cpp;
|
|
const std::error_code error = clang::format::parseConfiguration(content, &style);
|
|
QTC_ASSERT(error.value() == static_cast<int>(clang::format::ParseError::Success),
|
|
return defaultStyle);
|
|
|
|
addQtcStatementMacros(style);
|
|
std::string settings = clang::format::configurationAsText(style);
|
|
|
|
// Needed workaround because parseConfiguration remove BasedOnStyle field
|
|
// ToDo: standardize this behavior for future
|
|
const size_t index = content.find("BasedOnStyle");
|
|
if (index != std::string::npos) {
|
|
const size_t size = content.find("\n", index) - index;
|
|
const size_t insert_index = settings.find("\n");
|
|
settings.insert(insert_index, "\n" + content.substr(index, size));
|
|
}
|
|
|
|
return settings;
|
|
}
|
|
|
|
static std::map<QString, QString> getMapFromString(const QString &text)
|
|
{
|
|
std::map<QString, QString> objectNameMap;
|
|
|
|
QString parentName;
|
|
for (QString line : text.split('\n')) {
|
|
if (line.isEmpty())
|
|
continue;
|
|
|
|
QStringList list = line.split(':');
|
|
QString key = !list.isEmpty() ? list[0] : "";
|
|
QString value = line.mid(key.size() + 1).trimmed();
|
|
|
|
if (line.contains(':') && value.isEmpty()) {
|
|
parentName = key;
|
|
continue;
|
|
}
|
|
|
|
if (!value.isEmpty() && !line.startsWith(" "))
|
|
parentName = "";
|
|
|
|
if (line.startsWith(" - ") || line.startsWith(" ")) {
|
|
line.remove(0, 2);
|
|
if (objectNameMap.find(parentName) == objectNameMap.end())
|
|
objectNameMap[parentName] = line + "\n";
|
|
else
|
|
objectNameMap[parentName] += line + "\n";
|
|
continue;
|
|
}
|
|
|
|
if (line.startsWith(" ")) {
|
|
key.remove(0, 2);
|
|
key = parentName + key;
|
|
objectNameMap.insert(std::make_pair(key, value));
|
|
continue;
|
|
}
|
|
|
|
objectNameMap.insert(std::make_pair(key, value));
|
|
}
|
|
|
|
return objectNameMap;
|
|
}
|
|
|
|
void ClangFormatConfigWidget::fillTable()
|
|
{
|
|
Utils::GuardLocker locker(d->ignoreChanges);
|
|
|
|
const QString configText = QString::fromStdString(readFile(d->config->filePath().path()));
|
|
std::map<QString, QString> objectNameMap = getMapFromString(configText);
|
|
|
|
for (QObject *child : d->checksWidget->children()) {
|
|
if (!qobject_cast<QComboBox *>(child) && !qobject_cast<QLineEdit *>(child)
|
|
&& !qobject_cast<QPlainTextEdit *>(child)) {
|
|
continue;
|
|
}
|
|
|
|
if (objectNameMap.find(child->objectName()) == objectNameMap.end())
|
|
continue;
|
|
|
|
if (QPlainTextEdit *plainText = qobject_cast<QPlainTextEdit *>(child)) {
|
|
plainText->setPlainText(objectNameMap[child->objectName()]);
|
|
continue;
|
|
}
|
|
|
|
if (QComboBox *comboBox = qobject_cast<QComboBox *>(child)) {
|
|
if (comboBox->findText(objectNameMap[child->objectName()]) == -1) {
|
|
comboBox->setCurrentIndex(0);
|
|
} else {
|
|
comboBox->setCurrentText(objectNameMap[child->objectName()]);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (QLineEdit *lineEdit = qobject_cast<QLineEdit *>(child)) {
|
|
lineEdit->setText(objectNameMap[child->objectName()]);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ClangFormatConfigWidget::saveChanges(QObject *sender)
|
|
{
|
|
if (sender->objectName() == "BasedOnStyle") {
|
|
const auto *basedOnStyle = d->checksWidget->findChild<QComboBox *>("BasedOnStyle");
|
|
d->config->setBasedOnStyle(basedOnStyle->currentText());
|
|
} else {
|
|
QList<ClangFormatFile::Field> fields;
|
|
QString parentName;
|
|
|
|
for (QObject *child : d->checksWidget->children()) {
|
|
if (child->objectName() == "BasedOnStyle")
|
|
continue;
|
|
auto *label = qobject_cast<QLabel *>(child);
|
|
if (!label)
|
|
continue;
|
|
|
|
// reset parent name if label starts without " "
|
|
if (!label->text().startsWith(" "))
|
|
parentName = "";
|
|
|
|
QList<QWidget *> valueWidgets = d->checksWidget->findChildren<QWidget *>(
|
|
parentName + label->text().trimmed());
|
|
|
|
if (valueWidgets.empty()) {
|
|
// Currently BraceWrapping only.
|
|
fields.append({label->text(), ""});
|
|
// save parent name
|
|
parentName = label->text().trimmed();
|
|
continue;
|
|
}
|
|
|
|
QWidget *valueWidget = valueWidgets.first();
|
|
if (valueWidgets.size() > 1) {
|
|
for (QWidget *w : valueWidgets) {
|
|
if (w->objectName() == parentName + label->text().trimmed()) {
|
|
valueWidget = w;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!qobject_cast<QComboBox *>(valueWidget) && !qobject_cast<QLineEdit *>(valueWidget)
|
|
&& !qobject_cast<QPlainTextEdit *>(valueWidget)) {
|
|
continue;
|
|
}
|
|
|
|
auto *plainText = qobject_cast<QPlainTextEdit *>(valueWidget);
|
|
if (plainText) {
|
|
if (plainText->toPlainText().trimmed().isEmpty())
|
|
continue;
|
|
|
|
std::stringstream content;
|
|
QStringList list = plainText->toPlainText().split('\n');
|
|
for (const QString &line : list)
|
|
content << "\n " << line.toStdString();
|
|
|
|
fields.append({label->text(), QString::fromStdString(content.str())});
|
|
} else {
|
|
QString text;
|
|
if (auto *comboBox = qobject_cast<QComboBox *>(valueWidget)) {
|
|
text = comboBox->currentText();
|
|
} else {
|
|
auto *lineEdit = qobject_cast<QLineEdit *>(valueWidget);
|
|
QTC_ASSERT(lineEdit, continue;);
|
|
text = lineEdit->text();
|
|
}
|
|
|
|
if (!text.isEmpty() && text != "Default")
|
|
fields.append({label->text(), text});
|
|
}
|
|
}
|
|
d->config->changeFields(fields);
|
|
}
|
|
|
|
fillTable();
|
|
updatePreview();
|
|
synchronize();
|
|
}
|
|
|
|
void ClangFormatConfigWidget::setCodeStyleSettings(const CppEditor::CppCodeStyleSettings &settings)
|
|
{
|
|
d->config->fromCppCodeStyleSettings(settings);
|
|
|
|
fillTable();
|
|
updatePreview();
|
|
}
|
|
|
|
void ClangFormatConfigWidget::setTabSettings(const TextEditor::TabSettings &settings)
|
|
{
|
|
d->config->fromTabSettings(settings);
|
|
|
|
fillTable();
|
|
updatePreview();
|
|
}
|
|
|
|
void ClangFormatConfigWidget::synchronize()
|
|
{
|
|
emit codeStyleSettingsChanged(d->config->toCppCodeStyleSettings(d->project));
|
|
emit tabSettingsChanged(d->config->toTabSettings(d->project));
|
|
}
|
|
|
|
void ClangFormatConfigWidget::apply()
|
|
{
|
|
if (!d->checksWidget->isVisible() && !d->checksWidget->isEnabled())
|
|
return;
|
|
|
|
d->style = d->config->style();
|
|
}
|
|
|
|
void ClangFormatConfigWidget::finish()
|
|
{
|
|
if (!d->checksWidget->isVisible() && !d->checksWidget->isEnabled())
|
|
return;
|
|
|
|
d->config->setStyle(d->style);
|
|
}
|
|
|
|
} // namespace ClangFormat
|