Skip to content

Commit

Permalink
feat: add a find in files API to Project
Browse files Browse the repository at this point in the history
The API return a list of document + position of the find items.
The method uses ripgrep (rg) for searching, which must be installed and accessible in the system's PATH.
The `pattern` parameter should be a valid regular expression.
Fixes KDAB#21
  • Loading branch information
smnppKDAB committed Aug 7, 2024
1 parent 3831bd6 commit 911d9d9
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 0 deletions.
9 changes: 9 additions & 0 deletions docs/API/knut/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Knut
|array<string> |**[allFilesWithExtension](#allFilesWithExtension)**(string extension, PathType type = RelativeToRoot)|
|array<string> |**[allFilesWithExtensions](#allFilesWithExtensions)**(array<string> extensions, PathType type = RelativeToRoot)|
||**[closeAll](#closeAll)**()|
|QVariantList |**[findInFiles](#findInFiles)**(const QString &pattern)|
|[Document](../knut/document.md) |**[get](#get)**(string fileName)|
|[Document](../knut/document.md) |**[open](#open)**(string fileName)|
||**[openPrevious](#openPrevious)**(int index = 1)|
Expand Down Expand Up @@ -75,6 +76,14 @@ Returns all files with an extension from `extensions` in the current project.

Close all documents. If the document has some changes, save the changes.

#### <a name="findInFiles"></a>QVariantList **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").

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.

#### <a name="get"></a>[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
Expand Down
54 changes: 54 additions & 0 deletions src/core/project.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
#include <QDirIterator>
#include <QFileInfo>
#include <QMetaEnum>
#include <QProcess>
#include <QStandardPaths>
#include <algorithm>
#include <kdalgorithms.h>
#include <map>
Expand Down Expand Up @@ -380,4 +382,56 @@ Document *Project::openPrevious(int index)
LOG_RETURN("document", open(fileName));
}

/*!
* \qmlmethod QVariantList 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").
*
* 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;

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 to complete: {}", process.errorString());
return result;
}

QString output = process.readAllStandardOutput();

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) {
const auto parts = line.split(':');
if (parts.size() >= 3) {
QVariantMap matchResult;
matchResult.insert("file", QDir(m_root).relativeFilePath(parts[0]));
matchResult.insert("line", parts[1].toInt());
matchResult.insert("column", parts[2].toInt());
result.append(matchResult);
}
}
return result;
}
} // namespace Core
1 change: 1 addition & 0 deletions src/core/project.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ 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;

public slots:
Core::Document *get(const QString &fileName);
Expand Down
29 changes: 29 additions & 0 deletions test_data/tst_project.qml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,33 @@ Script {
var rcdoc = Project.open("MFC_UpdateGUI.rc")
compare(rcdoc.type, Document.Rc)
}

function test_findInFiles() {
let simplePattern = "CTutorialApp::InitInstance()"
let simpleResults = Project.findInFiles(simplePattern)

compare(simpleResults.length, 2)
if (simpleResults[0].file === "TutorialApp.cpp") {
compare(simpleResults[0].line, 21)
compare(simpleResults[0].column, 6)
compare(simpleResults[1].file, "TutorialDlg.h")
compare(simpleResults[1].line, 10)
compare(simpleResults[1].column, 9)
}
else {
compare(simpleResults[0].file, "TutorialDlg.h")
compare(simpleResults[0].line, 10)
compare(simpleResults[0].column, 9)
compare(simpleResults[1].file, "TutorialApp.cpp")
compare(simpleResults[1].line, 21)
compare(simpleResults[1].column, 6)
}

let multilinePattern = "m_VSliderBar\\.SetRange\\(0,\\s*100,\\s*TRUE\\);\\s*m_VSliderBar\\.SetPos\\(50\\);";
let multilineResults = Project.findInFiles(multilinePattern)

compare(multilineResults[0].file, "TutorialDlg.cpp")
compare(multilineResults[0].line, 65)
compare(multilineResults[0].column, 3)
}
}

0 comments on commit 911d9d9

Please sign in to comment.