// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 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(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(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 = 16; d->clangVersion = new QLabel(Tr::tr("Current clang-format version: ") + 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()->setSyntaxHighlighter(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(child); if (comboBox != nullptr) { connect(comboBox, &QComboBox::currentIndexChanged, this, std::bind(doSaveChanges, comboBox)); comboBox->installEventFilter(this); continue; } const auto button = qobject_cast(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 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(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(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 getMapFromString(const QString &text) { std::map 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 objectNameMap = getMapFromString(configText); for (QObject *child : d->checksWidget->children()) { if (!qobject_cast(child) && !qobject_cast(child) && !qobject_cast(child)) { continue; } if (objectNameMap.find(child->objectName()) == objectNameMap.end()) continue; if (QPlainTextEdit *plainText = qobject_cast(child)) { plainText->setPlainText(objectNameMap[child->objectName()]); continue; } if (QComboBox *comboBox = qobject_cast(child)) { if (comboBox->findText(objectNameMap[child->objectName()]) == -1) { comboBox->setCurrentIndex(0); } else { comboBox->setCurrentText(objectNameMap[child->objectName()]); } continue; } if (QLineEdit *lineEdit = qobject_cast(child)) { lineEdit->setText(objectNameMap[child->objectName()]); continue; } } } void ClangFormatConfigWidget::saveChanges(QObject *sender) { if (sender->objectName() == "BasedOnStyle") { const auto *basedOnStyle = d->checksWidget->findChild("BasedOnStyle"); d->config->setBasedOnStyle(basedOnStyle->currentText()); } else { QList fields; QString parentName; for (QObject *child : d->checksWidget->children()) { if (child->objectName() == "BasedOnStyle") continue; auto *label = qobject_cast(child); if (!label) continue; // reset parent name if label starts without " " if (!label->text().startsWith(" ")) parentName = ""; QList valueWidgets = d->checksWidget->findChildren( 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(valueWidget) && !qobject_cast(valueWidget) && !qobject_cast(valueWidget)) { continue; } auto *plainText = qobject_cast(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(valueWidget)) { text = comboBox->currentText(); } else { auto *lineEdit = qobject_cast(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