// 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 using namespace ProjectExplorer; using namespace Utils; namespace ClangFormat { class ClangFormatConfigWidget final : public CppEditor::CppCodeStyleWidget { public: ClangFormatConfigWidget(TextEditor::ICodeStylePreferences *codeStyle, Project *project, QWidget *parent); void apply() final; void finish() final; void setCodeStyleSettings(const CppEditor::CppCodeStyleSettings &settings) final; void setTabSettings(const TextEditor::TabSettings &settings) final; void synchronize() final; private: bool eventFilter(QObject *object, QEvent *event) final; FilePath globalPath(); FilePath projectPath(); void createStyleFileIfNeeded(bool isGlobal); void showOrHideWidgets(); void initChecksAndPreview(); void connectChecks(); void fillTable(); std::string readFile(const QString &path); void saveChanges(QObject *sender); void updatePreview(); void slotCodeStyleChanged(TextEditor::ICodeStylePreferences *currentPreferences); ProjectExplorer::Project *m_project = nullptr; QWidget *m_checksWidget = nullptr; QScrollArea *m_checksScrollArea = nullptr; TextEditor::SnippetEditorWidget *m_preview = nullptr; std::unique_ptr m_config; clang::format::FormatStyle m_style; Guard m_ignoreChanges; QLabel *m_fallbackConfig; QLabel *m_clangVersion; QLabel *m_clangWarningText; QLabel *m_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, Project *project, QWidget *parent) : CppCodeStyleWidget(parent) { m_project = project; m_config = std::make_unique(codeStyle->currentPreferences()); m_fallbackConfig = new QLabel(Tr::tr("Clang-Format Style")); m_checksScrollArea = new QScrollArea(); m_checksWidget = new ClangFormatChecks(); m_checksScrollArea->setWidget(m_checksWidget); m_checksScrollArea->setWidgetResizable(true); m_checksWidget->setEnabled(!codeStyle->isReadOnly() && !codeStyle->isTemporarilyReadOnly() && !codeStyle->isAdditionalTabDisabled()); static const int expectedMajorVersion = 17; m_clangVersion = new QLabel(Tr::tr("Current ClangFormat version: %1.").arg(LLVM_VERSION_STRING), this); m_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 = m_clangWarningText->palette(); palette.setColor(QPalette::WindowText, Qt::red); m_clangWarningText->setPalette(palette); m_clangWarningIcon = new QLabel(this); m_clangWarningIcon->setPixmap(Icons::WARNING.icon().pixmap(16, 16)); if (LLVM_VERSION_MAJOR == expectedMajorVersion) { m_clangWarningText->hide(); m_clangWarningIcon->hide(); } FilePath fileName; if (m_project) fileName = m_project->projectFilePath().pathAppended("snippet.cpp"); else fileName = Core::ICore::userResourcePath("snippet.cpp"); m_preview = new TextEditor::SnippetEditorWidget(this); TextEditor::DisplaySettings displaySettings = m_preview->displaySettings(); displaySettings.m_visualizeWhitespace = true; m_preview->setDisplaySettings(displaySettings); m_preview->setPlainText(QLatin1String(CppEditor::Constants::DEFAULT_CODE_STYLE_SNIPPETS[0])); auto *indenter = new ClangFormatIndenter(m_preview->document()); indenter->setOverriddenPreferences(codeStyle); m_preview->textDocument()->setIndenter(indenter); m_preview->textDocument()->setFontSettings(TextEditor::TextEditorSettings::fontSettings()); m_preview->textDocument()->resetSyntaxHighlighter( [] { return new CppEditor::CppHighlighter(); }); m_preview->textDocument()->indenter()->setFileName(fileName); using namespace Layouting; Column { m_fallbackConfig, Row {m_clangWarningIcon, m_clangWarningText, st}, m_clangVersion, Row { m_checksScrollArea, m_preview }, }.attachTo(this); connect(codeStyle, &TextEditor::ICodeStylePreferences::currentPreferencesChanged, this, &ClangFormatConfigWidget::slotCodeStyleChanged); slotCodeStyleChanged(codeStyle->currentPreferences()); showOrHideWidgets(); fillTable(); updatePreview(); connectChecks(); } void ClangFormatConfigWidget::slotCodeStyleChanged( TextEditor::ICodeStylePreferences *codeStyle) { if (!codeStyle) return; m_config.reset(new ClangFormatFile(codeStyle)); m_config->setIsReadOnly(codeStyle->isReadOnly()); m_style = m_config->style(); m_checksWidget->setEnabled(!codeStyle->isReadOnly() && !codeStyle->isTemporarilyReadOnly() && !codeStyle->isAdditionalTabDisabled()); fillTable(); updatePreview(); } void ClangFormatConfigWidget::connectChecks() { auto doSaveChanges = [this](QObject *sender) { if (!m_ignoreChanges.isLocked()) saveChanges(sender); }; for (QObject *child : m_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(); } FilePath ClangFormatConfigWidget::globalPath() { return Core::ICore::userResourcePath(); } FilePath ClangFormatConfigWidget::projectPath() { if (m_project) return globalPath().pathAppended("clang-format/" + projectUniqueId(m_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 = m_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(!m_project); m_fallbackConfig->show(); m_checksScrollArea->show(); m_preview->show(); } void ClangFormatConfigWidget::updatePreview() { QTextCursor cursor(m_preview->document()); cursor.setPosition(0); cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); m_preview->textDocument()->autoFormatOrIndent(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() { GuardLocker locker(m_ignoreChanges); const QString configText = QString::fromStdString(readFile(m_config->filePath().path())); std::map objectNameMap = getMapFromString(configText); for (QObject *child : m_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 = m_checksWidget->findChild("BasedOnStyle"); m_config->setBasedOnStyle(basedOnStyle->currentText()); } else { QList fields; QString parentName; for (QObject *child : m_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 = m_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}); } } m_config->changeFields(fields); } fillTable(); updatePreview(); synchronize(); } void ClangFormatConfigWidget::setCodeStyleSettings(const CppEditor::CppCodeStyleSettings &settings) { m_config->fromCppCodeStyleSettings(settings); fillTable(); updatePreview(); } void ClangFormatConfigWidget::setTabSettings(const TextEditor::TabSettings &settings) { m_config->fromTabSettings(settings); fillTable(); updatePreview(); } void ClangFormatConfigWidget::synchronize() { emit codeStyleSettingsChanged(m_config->toCppCodeStyleSettings(m_project)); emit tabSettingsChanged(m_config->toTabSettings(m_project)); } void ClangFormatConfigWidget::apply() { if (!m_checksWidget->isVisible() && !m_checksWidget->isEnabled()) return; m_style = m_config->style(); } void ClangFormatConfigWidget::finish() { if (!m_checksWidget->isVisible() && !m_checksWidget->isEnabled()) return; m_config->setStyle(m_style); } CppEditor::CppCodeStyleWidget *createClangFormatConfigWidget( TextEditor::ICodeStylePreferences *codeStyle, Project *project, QWidget *parent) { return new ClangFormatConfigWidget(codeStyle, project, parent); } } // namespace ClangFormat