diff --git a/docs/API/knut/cppdocument.md b/docs/API/knut/cppdocument.md index 268652e3..0fb49c69 100644 --- a/docs/API/knut/cppdocument.md +++ b/docs/API/knut/cppdocument.md @@ -298,6 +298,7 @@ The returned QueryMatch instances contain the following captures: - `declaration`: The full declaration of the method - `function`: The function declaration, without the return type - `name`: The name of the function +- `return-type`: The return type of the function without any reference/pointer specifiers (i.e. `&`/`*`) #### array<[QueryMatch](../knut/querymatch.md)> **queryMethodDefinition**(string scope, string methodName) diff --git a/src/core/codedocument.cpp b/src/core/codedocument.cpp index 8fac2a76..871da29b 100644 --- a/src/core/codedocument.cpp +++ b/src/core/codedocument.cpp @@ -848,4 +848,10 @@ AstNode CodeDocument::astNodeAt(int pos) return {}; } +QList CodeDocument::includedRanges() const +{ + // An empty list tells the parser to include the entire document. + return {}; +} + } // namespace Core diff --git a/src/core/codedocument.h b/src/core/codedocument.h index df1c476d..4a0dc4f6 100644 --- a/src/core/codedocument.h +++ b/src/core/codedocument.h @@ -15,6 +15,7 @@ #include "querymatch.h" #include "symbol.h" #include "textdocument.h" +#include "treesitter/parser.h" #include "treesitter/query.h" #include @@ -79,6 +80,8 @@ class CodeDocument : public TextDocument Q_INVOKABLE Core::AstNode astNodeAt(int pos); + virtual QList includedRanges() const; + public slots: void selectSymbol(const QString &name, int options = NoFindFlags); diff --git a/src/core/codedocument_p.cpp b/src/core/codedocument_p.cpp index ce0212c7..bcfd8f55 100644 --- a/src/core/codedocument_p.cpp +++ b/src/core/codedocument_p.cpp @@ -48,7 +48,12 @@ treesitter::Parser &TreeSitterHelper::parser() std::optional &TreeSitterHelper::syntaxTree() { if (!m_tree) { - m_tree = parser().parseString(m_document->text()); + auto &parser = this->parser(); + if (!parser.setIncludedRanges(m_document->includedRanges())) { + spdlog::warn("TreeSitterHelper::syntaxTree: Unable to set the included ranges on the treesitter parser!"); + parser.setIncludedRanges({}); + } + m_tree = parser.parseString(m_document->text()); if (!m_tree) { spdlog::warn("CodeDocument::syntaxTree: Failed to parse document {}!", m_document->fileName()); } diff --git a/src/core/core/settings.json b/src/core/core/settings.json index 9fc1a0b1..d448136b 100644 --- a/src/core/core/settings.json +++ b/src/core/core/settings.json @@ -30,6 +30,13 @@ "LANG_NEUTRAL": "[default]" } }, + "cpp": { + "excluded_macros": [ + "AFX_EXT_CLASS", + "[A-Z_]*EXPORT[A-Z_]*", + "Q_OBJECT" + ] + }, "mime_types": { "c": "cpp_type", "cpp": "cpp_type", diff --git a/src/core/cppdocument.cpp b/src/core/cppdocument.cpp index 656c9847..a42b0687 100644 --- a/src/core/cppdocument.cpp +++ b/src/core/cppdocument.cpp @@ -692,6 +692,7 @@ MessageMap CppDocument::mfcExtractMessageMap(const QString &className /* = ""*/) * - `declaration`: The full declaration of the method * - `function`: The function declaration, without the return type * - `name`: The name of the function + * - `return-type`: The return type of the function without any reference/pointer specifiers (i.e. `&`/`*`) */ Core::QueryMatchList CppDocument::queryMethodDeclaration(const QString &className, const QString &functionName) { @@ -1545,4 +1546,75 @@ QStringList CppDocument::primitiveTypes() const return Utils::cppPrimitiveTypes(); } +QList CppDocument::includedRanges() const +{ + auto macros = Settings::instance()->value(Settings::CppExcludedMacros); + if (macros.isEmpty()) { + return {}; + } + + QRegularExpression regex(macros.join("|")); + if (!regex.isValid()) { + spdlog::error("CppDocument::includedRanges: Failed to create regex for excluded macros: {}", + regex.errorString()); + return {}; + } + + auto document = textEdit()->document(); + + QList ranges; + treesitter::Point lastPoint {0, 0}; + uint32_t lastByte = 0; + + for (auto block = document->firstBlock(); block.isValid(); block = block.next()) { + QRegularExpressionMatch match; + auto searchFrom = 0; + auto index = block.text().indexOf(regex, searchFrom, &match); + + // Run this in a loop to support multiple macros on the same line. + while (index != -1) { + // We need to construct a range from the end of the last match to the start of the current match. + // + // Note that the ranges have an inclusive start and an exclusive end.. + // + // Also Note that the column seems to be in bytes, not characters. + // This is why we multiply by sizeof(QChar) to get the correct column. + // At least that's what the TreeSitterInspector shows us. + auto endPoint = treesitter::Point {.row = static_cast(block.blockNumber()), + .column = static_cast(index * sizeof(QChar))}; + ranges.push_back({.start_point = lastPoint, + .end_point = endPoint, + .start_byte = lastByte, + // No need to add - 1 here, the ranges are exclusive at the end. + .end_byte = static_cast((block.position() + index) * sizeof(QChar))}); + + auto matchLength = match.capturedLength(); + lastByte = static_cast((block.position() + index + matchLength) * sizeof(QChar)); + lastPoint = {.row = static_cast(block.blockNumber()), + .column = static_cast((index + matchLength) * sizeof(QChar))}; + if (lastPoint.column == static_cast(block.length())) { + ++lastPoint.row; + lastPoint.column = 0; + } + + searchFrom = index + matchLength; + index = block.text().indexOf(regex, searchFrom, &match); + } + } + + if (!ranges.isEmpty()) { + // Add the last range, up to the end of the document, but only if we have another range. + // Leaving the ranges empty will parse the entire document, so that's easiest. + auto endPoint = + treesitter::Point {.row = static_cast(document->blockCount() - 1), + .column = static_cast(document->lastBlock().length() * sizeof(QChar))}; + ranges.push_back({.start_point = lastPoint, + .end_point = endPoint, + .start_byte = lastByte, + .end_byte = static_cast(document->characterCount() * sizeof(QChar))}); + } + + return ranges; +} + } // namespace Core diff --git a/src/core/cppdocument.h b/src/core/cppdocument.h index 00e021db..cab1f701 100644 --- a/src/core/cppdocument.h +++ b/src/core/cppdocument.h @@ -60,6 +60,8 @@ class CppDocument : public CodeDocument bool changeBaseClass(CppDocument *header, CppDocument *source, const QString &className, const QString &newClassBaseName); + QList includedRanges() const override; + public slots: Core::CppDocument *openHeaderSource(); diff --git a/src/core/settings.h b/src/core/settings.h index 463db5d3..0a332311 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -49,6 +49,7 @@ class Settings : public QObject static inline constexpr char RcAssetFlags[] = "/rc/asset_flags"; static inline constexpr char RcAssetColors[] = "/rc/asset_transparent_colors"; static inline constexpr char RcLanguageMap[] = "/rc/language_map"; + static inline constexpr char CppExcludedMacros[] = "/cpp/excluded_macros"; static inline constexpr char SaveLogsToFile[] = "/logs/saveToFile"; static inline constexpr char ScriptPaths[] = "/script_paths"; static inline constexpr char Tab[] = "/text_editor/tab"; diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index b7a57bb2..e4eb0ed2 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -19,7 +19,9 @@ #include #include #include +#include #include +#include #include #include @@ -45,6 +47,7 @@ OptionsDialog::OptionsDialog(QWidget *parent) initializeRcSettings(); initializeSaveToLogFileSetting(); initializeEnableLSPSetting(); + initializeCppSettings(); updateScriptPaths(); } @@ -217,6 +220,42 @@ void OptionsDialog::initializeRcSettings() }); } +void OptionsDialog::initializeCppSettings() +{ + auto *model = new QStringListModel(this); + model->setStringList(DEFAULT_VALUE(QStringList, CppExcludedMacros)); + ui->cppExcludedMacros->setModel(model); + ui->cppExcludedMacros->setSelectionMode(QAbstractItemView::SingleSelection); + + connect(ui->cppAddExcludedMacro, &QPushButton::clicked, model, [this, model]() { + auto ok = false; + auto excludeMacro = QInputDialog::getText(this, tr("Exclude a new macro"), tr("Macro name (supports Regex)"), + QLineEdit::Normal, "", &ok); + if (ok) { + if (model->insertRows(0, 1)) { + model->setData(model->index(0), excludeMacro); + } else { + spdlog::warn("OptionsDialog::excludeMacro: Failed to create new row!"); + } + } + }); + + connect(ui->cppRemoveExcludedMacro, &QPushButton::clicked, model, [this, model]() { + auto selection = ui->cppExcludedMacros->selectionModel()->selectedRows(); + // single selection mode, so should be at most one. + if (!selection.isEmpty()) { + model->removeRows(selection.front().row(), 1); + } + }); + + connect(model, &QStringListModel::dataChanged, model, [model]() { + SET_DEFAULT_VALUE(CppExcludedMacros, model->stringList()); + }); + connect(model, &QStringListModel::rowsRemoved, model, [model]() { + SET_DEFAULT_VALUE(CppExcludedMacros, model->stringList()); + }); +} + void OptionsDialog::openUserSettings() { QDesktopServices::openUrl(QUrl::fromLocalFile(ui->userPath->text())); diff --git a/src/gui/optionsdialog.h b/src/gui/optionsdialog.h index c05f7020..d7108b5e 100644 --- a/src/gui/optionsdialog.h +++ b/src/gui/optionsdialog.h @@ -35,6 +35,7 @@ class OptionsDialog : public QDialog void initializeScriptPathSettings(); void initializeScriptBehaviorSettings(); void initializeRcSettings(); + void initializeCppSettings(); void openUserSettings(); void openProjectSettings(); diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index 32561bba..2d2f4f7c 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -626,6 +626,49 @@ + + + + + + C++ Files + + + + + + The Macros listed here will be excluded from parsing (supports Regex). + + + Excluded Macros + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Add + + + + + + + Remove + + + + + + + + + + + @@ -684,6 +727,11 @@ Rc Files + + + C++ Files + + diff --git a/src/gui/treesitterinspector.cpp b/src/gui/treesitterinspector.cpp index 944cc479..49310a49 100644 --- a/src/gui/treesitterinspector.cpp +++ b/src/gui/treesitterinspector.cpp @@ -182,6 +182,7 @@ void TreeSitterInspector::changeText() Core::LoggerDisabler disableLogging; text = m_document->text(); } + m_parser.setIncludedRanges(m_document->includedRanges()); auto tree = m_parser.parseString(text); if (tree.has_value()) { m_treemodel.setTree(std::move(tree.value()), makePredicates(), ui->enableUnnamed->isChecked()); diff --git a/src/gui/treesittertreemodel.cpp b/src/gui/treesittertreemodel.cpp index 9341a8de..d38acc82 100644 --- a/src/gui/treesittertreemodel.cpp +++ b/src/gui/treesittertreemodel.cpp @@ -74,11 +74,13 @@ QVariant TreeSitterTreeModel::TreeNode::data(int column) const return QString("%1: %2").arg(fieldName, m_node.type()); } case 1: - return QString("[%1:%2] - [%3:%4]") + return QString("[%1:%2](%3) - [%4:%5](%6)") .arg(m_node.startPoint().row) .arg(m_node.startPoint().column) + .arg(m_node.startPosition()) .arg(m_node.endPoint().row) - .arg(m_node.endPoint().column); + .arg(m_node.endPoint().column) + .arg(m_node.endPosition()); default: break; } diff --git a/src/treesitter/parser.cpp b/src/treesitter/parser.cpp index 279ae912..11959df2 100644 --- a/src/treesitter/parser.cpp +++ b/src/treesitter/parser.cpp @@ -58,6 +58,11 @@ std::optional Parser::parseString(const QString &text, const Tree *old_tre return tree ? Tree(tree) : std::optional {}; } +bool Parser::setIncludedRanges(const QList &ranges) +{ + return ts_parser_set_included_ranges(m_parser, ranges.data(), ranges.size()); +} + const TSLanguage *Parser::language() const { return ts_parser_language(m_parser); diff --git a/src/treesitter/parser.h b/src/treesitter/parser.h index 7ef69fe9..a9112e8e 100644 --- a/src/treesitter/parser.h +++ b/src/treesitter/parser.h @@ -12,6 +12,8 @@ #include "core/document.h" #include +#include +#include struct TSParser; struct TSLanguage; @@ -19,6 +21,7 @@ struct TSLanguage; namespace treesitter { class Tree; +using Range = TSRange; class Parser { @@ -37,6 +40,21 @@ class Parser std::optional parseString(const QString &text, const Tree *old_tree = nullptr) const; + /** + * Parse only the given ranges. + * Note: if the ranges are empty, the entire document is parsed. + * + * From tree_sitter/api.h: + * [The] given ranges must be ordered from earliest to latest in the document, + * and they must not overlap. That is, the following must hold for all + * `i` < `length - 1`: ranges[i].end_byte <= ranges[i + 1].start_byte + * + * If this requirement is not satisfied, the operation will fail, the ranges + * will not be assigned, and this function will return `false`. On success, + * this function returns `true` + */ + bool setIncludedRanges(const QList &ranges); + const TSLanguage *language() const; static TSLanguage *getLanguage(Core::Document::Type type); diff --git a/test_data/tst_cppdocument/treesitterExcludesMacros/AFX_EXT_CLASS.h b/test_data/tst_cppdocument/treesitterExcludesMacros/AFX_EXT_CLASS.h new file mode 100644 index 00000000..4c6df75c --- /dev/null +++ b/test_data/tst_cppdocument/treesitterExcludesMacros/AFX_EXT_CLASS.h @@ -0,0 +1,10 @@ +#pragma once + +class AFX_EXT_CLASS TestClass : public AFX_EXT_CLASSBase{ +publicAFX_EXT_CLASS: + void testMethod(); + +private: + int AFX_EXT_CLASS m_count; +}; +AFX_EXT_CLASS diff --git a/tests/tst_cppdocument_treesitter.cpp b/tests/tst_cppdocument_treesitter.cpp index b250a634..5ae65991 100644 --- a/tests/tst_cppdocument_treesitter.cpp +++ b/tests/tst_cppdocument_treesitter.cpp @@ -358,6 +358,29 @@ private slots: QVERIFY(headerFile.compare()); } } + + void excludeMacros() + { + Test::testCppDocument("tst_cppdocument/treesitterExcludesMacros", "AFX_EXT_CLASS.h", [](auto *document) { + auto match = document->queryClassDefinition("TestClass"); + QVERIFY(!match.isEmpty()); + QCOMPARE(match.get("name").text(), "TestClass"); + QCOMPARE(match.get("base").text(), "Base"); + + match = document->queryMember("TestClass", "m_count"); + QVERIFY(!match.isEmpty()); + QCOMPARE(match.get("name").text(), "m_count"); + QCOMPARE(match.get("type").text(), "int"); + QCOMPARE(match.get("member").text(), "int AFX_EXT_CLASS m_count;"); + + auto matches = document->queryMethodDeclaration("TestClass", "testMethod"); + QCOMPARE(matches.length(), 1); + match = matches.front(); + QVERIFY(!match.isEmpty()); + QCOMPARE(match.get("name").text(), "testMethod"); + QCOMPARE(match.get("return-type").text(), "void"); + }); + } }; QTEST_MAIN(TestCppDocumentTreeSitter)