Skip to content

Commit

Permalink
feat: add a find in files API to Project (#143)
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 PATH.
The `pattern` parameter should be a valid regular expression.
Fixes #21
  • Loading branch information
smnppKDAB authored Aug 21, 2024
1 parent 8227867 commit 6041dbf
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 0 deletions.
25 changes: 25 additions & 0 deletions docs/API/knut/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)**()|
Expand Down Expand Up @@ -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.

#### <a name="findInFiles"></a>array&lt;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.

#### <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 All @@ -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.

#### <a name="isFindInFilesAvailable"></a>bool **isFindInFilesAvailable**()

Checks if the ripgrep (rg) command-line tool is available on the system.

#### <a name="open"></a>[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
Expand Down
93 changes: 93 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,95 @@ Document *Project::openPrevious(int index)
LOG_RETURN("document", open(fileName));
}

/*!
* \qmlmethod array<object> 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
2 changes: 2 additions & 0 deletions src/core/project.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
30 changes: 30 additions & 0 deletions test_data/tst_project.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}

0 comments on commit 6041dbf

Please sign in to comment.