diff --git a/docs/API/knut/project.md b/docs/API/knut/project.md index b8acfb2e..c8819b3a 100644 --- a/docs/API/knut/project.md +++ b/docs/API/knut/project.md @@ -22,7 +22,9 @@ import Knut |array<string> |**[allFilesWithExtension](#allFilesWithExtension)**(string extension, PathType type = RelativeToRoot)| |array<string> |**[allFilesWithExtensions](#allFilesWithExtensions)**(array<string> extensions, PathType type = RelativeToRoot)| ||**[closeAll](#closeAll)**()| +|array<object> |**[findInFiles](#findInFiles)**(const QString &pattern)| |[Document](../knut/document.md) |**[get](#get)**(string fileName)| +|bool |**[isFindInFilesAvailable](#isFindInFilesAvailable)**()| |[Document](../knut/document.md) |**[open](#open)**(string fileName)| ||**[openPrevious](#openPrevious)**(int index = 1)| ||**[saveAllDocuments](#saveAllDocuments)**()| @@ -75,6 +77,25 @@ Returns all files with an extension from `extensions` in the current project. Close all documents. If the document has some changes, save the changes. +#### array<object> **findInFiles**(const QString &pattern) + +Search for a regex pattern in all files of the current project using ripgrep. +Returns a list of results (QVariantMaps) with the document name and position ("file", "line", "column"). + +Example usage in QML: + +```js +let findResults = Project.findInFiles("foo"); +for (let result of findResults) { + Message.log("Filename: " + result.file); + Message.log("Line: " + result.line); + Message.log("Column" + result.column); +} +``` + +Note: The method uses ripgrep (rg) for searching, which must be installed and accessible in PATH. +The `pattern` parameter should be a valid regular expression. + #### [Document](../knut/document.md) **get**(string fileName) Gets the document for the given `fileName`. If the document is not opened yet, open it. If the document @@ -86,6 +107,10 @@ If the document does not exist, creates a new document (but don't save it yet). !!! note This command does not change the current document. +#### bool **isFindInFilesAvailable**() + +Checks if the ripgrep (rg) command-line tool is available on the system. + #### [Document](../knut/document.md) **open**(string fileName) Opens or creates a document for the given `fileName` and make it current. If the document is already opened, returns diff --git a/src/core/project.cpp b/src/core/project.cpp index f6fecd56..aa537fe9 100644 --- a/src/core/project.cpp +++ b/src/core/project.cpp @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include #include #include @@ -380,4 +382,95 @@ Document *Project::openPrevious(int index) LOG_RETURN("document", open(fileName)); } +/*! + * \qmlmethod array Project::findInFiles(const QString &pattern) + * Search for a regex pattern in all files of the current project using ripgrep. + * Returns a list of results (QVariantMaps) with the document name and position ("file", "line", "column"). + * + * Example usage in QML: + * + * ```js + * let findResults = Project.findInFiles("foo"); + * for (let result of findResults) { + * Message.log("Filename: " + result.file); + * Message.log("Line: " + result.line); + * Message.log("Column" + result.column); + * } + * ``` + * + * Note: The method uses ripgrep (rg) for searching, which must be installed and accessible in PATH. + * The `pattern` parameter should be a valid regular expression. + */ +QVariantList Project::findInFiles(const QString &pattern) const +{ + LOG("Project::findInFiles", pattern); + + QVariantList result; + + const QString path = QStandardPaths::findExecutable("rg"); + if (path.isEmpty()) { + spdlog::error("Ripgrep (rg) executable not found. Please ensure that ripgrep is installed and its location is " + "included in the PATH environment variable."); + return result; + } + + QProcess process; + + const QStringList arguments {"--vimgrep", "-U", "--multiline-dotall", pattern, m_root}; + + process.start(path, arguments); + if (!process.waitForFinished()) { + spdlog::error("The ripgrep process failed: {}", process.errorString()); + return result; + } + + const QString output = process.readAllStandardOutput(); + + const QString errorOutput = process.readAllStandardError(); + if (!errorOutput.isEmpty()) { + spdlog::error("Ripgrep error: {}", errorOutput); + } + + const auto lines = output.split('\n', Qt::SkipEmptyParts); + result.reserve(lines.count() * 3); + for (const QString &line : lines) { + QString currentLine = line; + currentLine.replace('\\', '/'); + const auto parts = currentLine.split(':'); + + QString filePath; + int offset = 0; + if (parts.size() > 2 && parts[0].length() == 1 && parts[1].startsWith('/')) { + filePath = parts[0] + ':' + parts[1]; + offset = 2; + } else { + filePath = parts[0]; + offset = 1; + } + + if (parts.size() > offset + 1) { + QVariantMap matchResult; + matchResult.insert("file", filePath); + matchResult.insert("line", parts[offset].toInt()); + matchResult.insert("column", parts[offset + 1].toInt()); + result.append(matchResult); + } + } + return result; +} + +/*! + * \qmlmethod bool Project::isFindInFilesAvailable() + * Checks if the ripgrep (rg) command-line tool is available on the system. + */ +bool Project::isFindInFilesAvailable() const +{ + QString rgPath = QStandardPaths::findExecutable("rg"); + if (rgPath.isEmpty()) { + return false; + } else { + return true; + } +} + } // namespace Core diff --git a/src/core/project.h b/src/core/project.h index 1b6804d4..dbecde72 100644 --- a/src/core/project.h +++ b/src/core/project.h @@ -53,6 +53,8 @@ class Project : public QObject Core::Project::PathType type = RelativeToRoot); Q_INVOKABLE QStringList allFilesWithExtensions(const QStringList &extensions, Core::Project::PathType type = RelativeToRoot); + Q_INVOKABLE QVariantList findInFiles(const QString &pattern) const; + Q_INVOKABLE bool isFindInFilesAvailable() const; public slots: Core::Document *get(const QString &fileName); diff --git a/test_data/tst_project.qml b/test_data/tst_project.qml index 6392ec99..b0e0c4af 100644 --- a/test_data/tst_project.qml +++ b/test_data/tst_project.qml @@ -33,4 +33,34 @@ Script { var rcdoc = Project.open("MFC_UpdateGUI.rc") compare(rcdoc.type, Document.Rc) } + + function test_findInFiles() { + if(Project.isFindInFilesAvailable()) { + let simplePattern = "CTutorialApp::InitInstance()" + let simpleResults = Project.findInFiles(simplePattern) + + compare(simpleResults.length, 2) + + simpleResults.sort((a, b) => a.file.localeCompare(b.file)); + + compare(simpleResults[0].file, Project.root + "/TutorialApp.cpp") + compare(simpleResults[0].line, 21) + compare(simpleResults[0].column, 6) + compare(simpleResults[1].file, Project.root + "/TutorialDlg.h") + compare(simpleResults[1].line, 10) + compare(simpleResults[1].column, 9) + + let multilinePattern = "m_VSliderBar\\.SetRange\\(0,\\s*100,\\s*TRUE\\);\\s*m_VSliderBar\\.SetPos\\(50\\);"; + let multilineResults = Project.findInFiles(multilinePattern) + + compare(multilineResults.length, 1) + + compare(multilineResults[0].file, Project.root + "/TutorialDlg.cpp") + compare(multilineResults[0].line, 65) + compare(multilineResults[0].column, 3) + } + else { + Message.warning("Ripgrep (rg) isn't available on the system") + } + } }