mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-02-23 23:43:05 -05:00
feat: Add file search to chat
feat: Add opened documents files
This commit is contained in:
@ -20,6 +20,7 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
|
|
||||||
qml/controls/AttachedFilesPlace.qml
|
qml/controls/AttachedFilesPlace.qml
|
||||||
qml/controls/BottomBar.qml
|
qml/controls/BottomBar.qml
|
||||||
|
qml/controls/FileMentionPopup.qml
|
||||||
qml/controls/FileEditsActionBar.qml
|
qml/controls/FileEditsActionBar.qml
|
||||||
qml/controls/ContextViewer.qml
|
qml/controls/ContextViewer.qml
|
||||||
qml/controls/Toast.qml
|
qml/controls/Toast.qml
|
||||||
@ -68,6 +69,7 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
FileItem.hpp FileItem.cpp
|
FileItem.hpp FileItem.cpp
|
||||||
ChatFileManager.hpp ChatFileManager.cpp
|
ChatFileManager.hpp ChatFileManager.cpp
|
||||||
ChatCompressor.hpp ChatCompressor.cpp
|
ChatCompressor.hpp ChatCompressor.cpp
|
||||||
|
FileMentionItem.hpp FileMentionItem.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(QodeAssistChatView
|
target_link_libraries(QodeAssistChatView
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -21,9 +21,12 @@
|
|||||||
|
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include <QTextStream>
|
||||||
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
#include <coreplugin/icore.h>
|
#include <coreplugin/icore.h>
|
||||||
@ -225,6 +228,18 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
this,
|
this,
|
||||||
&ChatRootView::refreshRules);
|
&ChatRootView::refreshRules);
|
||||||
|
|
||||||
|
connect(
|
||||||
|
ProjectExplorer::ProjectManager::instance(),
|
||||||
|
&ProjectExplorer::ProjectManager::projectAdded,
|
||||||
|
this,
|
||||||
|
&ChatRootView::openFilesChanged);
|
||||||
|
|
||||||
|
connect(
|
||||||
|
ProjectExplorer::ProjectManager::instance(),
|
||||||
|
&ProjectExplorer::ProjectManager::projectRemoved,
|
||||||
|
this,
|
||||||
|
&ChatRootView::openFilesChanged);
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
&Settings::chatAssistantSettings().enableChatTools,
|
&Settings::chatAssistantSettings().enableChatTools,
|
||||||
&Utils::BaseAspect::changed,
|
&Utils::BaseAspect::changed,
|
||||||
@ -738,6 +753,13 @@ void ChatRootView::openSettings()
|
|||||||
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID);
|
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatRootView::openFileInEditor(const QString &filePath)
|
||||||
|
{
|
||||||
|
if (filePath.isEmpty())
|
||||||
|
return;
|
||||||
|
Core::EditorManager::openEditor(Utils::FilePath::fromString(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
void ChatRootView::updateInputTokensCount()
|
void ChatRootView::updateInputTokensCount()
|
||||||
{
|
{
|
||||||
int inputTokens = m_messageTokensCount;
|
int inputTokens = m_messageTokensCount;
|
||||||
@ -788,6 +810,8 @@ void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
|
|||||||
if (editor) {
|
if (editor) {
|
||||||
m_currentEditors.removeOne(editor);
|
m_currentEditors.removeOne(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emit openFilesChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
|
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
|
||||||
@ -805,6 +829,7 @@ void ChatRootView::onEditorCreated(Core::IEditor *editor, const Utils::FilePath
|
|||||||
{
|
{
|
||||||
if (editor && editor->document()) {
|
if (editor && editor->document()) {
|
||||||
m_currentEditors.append(editor);
|
m_currentEditors.append(editor);
|
||||||
|
emit openFilesChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -20,6 +20,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QQuickItem>
|
#include <QQuickItem>
|
||||||
|
#include <QVariantList>
|
||||||
|
|
||||||
#include "ChatFileManager.hpp"
|
#include "ChatFileManager.hpp"
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
@ -104,6 +105,8 @@ public:
|
|||||||
Q_INVOKABLE void openRulesFolder();
|
Q_INVOKABLE void openRulesFolder();
|
||||||
Q_INVOKABLE void openSettings();
|
Q_INVOKABLE void openSettings();
|
||||||
|
|
||||||
|
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
||||||
|
|
||||||
Q_INVOKABLE void updateInputTokensCount();
|
Q_INVOKABLE void updateInputTokensCount();
|
||||||
int inputTokensCount() const;
|
int inputTokensCount() const;
|
||||||
|
|
||||||
@ -222,6 +225,8 @@ signals:
|
|||||||
void compressionCompleted(const QString &compressedChatPath);
|
void compressionCompleted(const QString &compressedChatPath);
|
||||||
void compressionFailed(const QString &error);
|
void compressionFailed(const QString &error);
|
||||||
|
|
||||||
|
void openFilesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||||
QString getChatsHistoryDir() const;
|
QString getChatsHistoryDir() const;
|
||||||
|
|||||||
401
ChatView/FileMentionItem.cpp
Normal file
401
ChatView/FileMentionItem.cpp
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "FileMentionItem.hpp"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QTextStream>
|
||||||
|
|
||||||
|
#include <coreplugin/editormanager/documentmodel.h>
|
||||||
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
FileMentionItem::FileMentionItem(QQuickItem *parent)
|
||||||
|
: QQuickItem(parent)
|
||||||
|
{}
|
||||||
|
|
||||||
|
QVariantList FileMentionItem::searchResults() const
|
||||||
|
{
|
||||||
|
return m_searchResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FileMentionItem::currentIndex() const
|
||||||
|
{
|
||||||
|
return m_currentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::setCurrentIndex(int index)
|
||||||
|
{
|
||||||
|
if (m_currentIndex == index)
|
||||||
|
return;
|
||||||
|
m_currentIndex = index;
|
||||||
|
emit currentIndexChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::updateSearch(const QString &query)
|
||||||
|
{
|
||||||
|
m_lastQuery = query;
|
||||||
|
|
||||||
|
QVariantList openFiles = getOpenFiles(query);
|
||||||
|
QVariantList projectResults = searchProjectFiles(query);
|
||||||
|
|
||||||
|
QSet<QString> openPaths;
|
||||||
|
for (const QVariant &item : std::as_const(openFiles)) {
|
||||||
|
const QVariantMap map = item.toMap();
|
||||||
|
openPaths.insert(map.value("absolutePath").toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList combined = openFiles;
|
||||||
|
for (const QVariant &item : std::as_const(projectResults)) {
|
||||||
|
const QVariantMap map = item.toMap();
|
||||||
|
if (!map.value("isProject").toBool()
|
||||||
|
&& openPaths.contains(map.value("absolutePath").toString()))
|
||||||
|
continue;
|
||||||
|
combined.append(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_searchResults = combined;
|
||||||
|
m_currentIndex = 0;
|
||||||
|
emit searchResultsChanged();
|
||||||
|
emit currentIndexChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::refreshSearch()
|
||||||
|
{
|
||||||
|
if (!m_lastQuery.isNull())
|
||||||
|
updateSearch(m_lastQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::moveUp()
|
||||||
|
{
|
||||||
|
if (m_currentIndex > 0) {
|
||||||
|
m_currentIndex--;
|
||||||
|
emit currentIndexChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::moveDown()
|
||||||
|
{
|
||||||
|
if (m_currentIndex < m_searchResults.size() - 1) {
|
||||||
|
m_currentIndex++;
|
||||||
|
emit currentIndexChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::selectCurrent()
|
||||||
|
{
|
||||||
|
if (m_currentIndex < 0 || m_currentIndex >= m_searchResults.size())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QVariantMap item = m_searchResults[m_currentIndex].toMap();
|
||||||
|
if (item.value("isProject").toBool()) {
|
||||||
|
emit projectSelected(item.value("projectName").toString());
|
||||||
|
} else {
|
||||||
|
emit fileSelected(
|
||||||
|
item.value("absolutePath").toString(),
|
||||||
|
item.value("relativePath").toString(),
|
||||||
|
item.value("projectName").toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::dismiss()
|
||||||
|
{
|
||||||
|
m_searchResults.clear();
|
||||||
|
m_currentIndex = 0;
|
||||||
|
emit searchResultsChanged();
|
||||||
|
emit currentIndexChanged();
|
||||||
|
emit dismissed();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap FileMentionItem::handleFileSelection(
|
||||||
|
const QString &absolutePath,
|
||||||
|
const QString &relativePath,
|
||||||
|
const QString &projectName,
|
||||||
|
const QString ¤tQuery,
|
||||||
|
bool useTools)
|
||||||
|
{
|
||||||
|
QVariantMap result;
|
||||||
|
const QString fileName = relativePath.section('/', -1);
|
||||||
|
|
||||||
|
QString mentionKey = fileName;
|
||||||
|
const int colonIdx = currentQuery.indexOf(':');
|
||||||
|
if (colonIdx > 0) {
|
||||||
|
const QString projPrefix = currentQuery.left(colonIdx);
|
||||||
|
if (projPrefix.compare(projectName, Qt::CaseInsensitive) == 0)
|
||||||
|
mentionKey = projPrefix + ":" + fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useTools) {
|
||||||
|
registerMention(mentionKey, absolutePath);
|
||||||
|
result["mode"] = QStringLiteral("mention");
|
||||||
|
result["mentionText"] = "@" + mentionKey + " ";
|
||||||
|
} else {
|
||||||
|
emit fileAttachRequested({absolutePath});
|
||||||
|
result["mode"] = QStringLiteral("attach");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::registerMention(const QString &mentionKey, const QString &absolutePath)
|
||||||
|
{
|
||||||
|
m_atMentionMap[mentionKey] = absolutePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::clearMentions()
|
||||||
|
{
|
||||||
|
m_atMentionMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FileMentionItem::expandMentions(const QString &text)
|
||||||
|
{
|
||||||
|
QString result = text;
|
||||||
|
|
||||||
|
for (auto it = m_atMentionMap.constBegin(); it != m_atMentionMap.constEnd(); ++it) {
|
||||||
|
const QString &mentionKey = it.key();
|
||||||
|
const QString &absPath = it.value();
|
||||||
|
const QString displayName = mentionKey.section(':', -1);
|
||||||
|
const QString escaped = QRegularExpression::escape(mentionKey);
|
||||||
|
|
||||||
|
// @key:N-M -> hyperlink + inline code block
|
||||||
|
const QRegularExpression rangeRe("@" + escaped + ":(\\d+)-(\\d+)(?=\\s|$)");
|
||||||
|
QRegularExpressionMatchIterator matchIt = rangeRe.globalMatch(result);
|
||||||
|
QList<QRegularExpressionMatch> matches;
|
||||||
|
while (matchIt.hasNext())
|
||||||
|
matches.append(matchIt.next());
|
||||||
|
|
||||||
|
for (int i = matches.size() - 1; i >= 0; --i) {
|
||||||
|
const auto &m = matches[i];
|
||||||
|
const int startLine = m.captured(1).toInt();
|
||||||
|
const int endLine = m.captured(2).toInt();
|
||||||
|
const QString ext = fileExtension(absPath);
|
||||||
|
const QString snippet = readFileLines(absPath, startLine, endLine);
|
||||||
|
const QString replacement
|
||||||
|
= QString("[@%1:%2-%3](file://%4)\n```%5\n%6```")
|
||||||
|
.arg(displayName)
|
||||||
|
.arg(startLine)
|
||||||
|
.arg(endLine)
|
||||||
|
.arg(absPath, ext, snippet);
|
||||||
|
result.replace(m.capturedStart(), m.capturedLength(), replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @key -> hyperlink only
|
||||||
|
const QRegularExpression simpleRe("@" + escaped + "(?=\\s|$)");
|
||||||
|
result.replace(simpleRe, QString("[@%1](file://%2)").arg(displayName, absPath));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList FileMentionItem::searchProjectFiles(const QString &query)
|
||||||
|
{
|
||||||
|
QVariantList results;
|
||||||
|
|
||||||
|
struct FileResult
|
||||||
|
{
|
||||||
|
QString absolutePath;
|
||||||
|
QString relativePath;
|
||||||
|
QString projectName;
|
||||||
|
int priority;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto allProjects = ProjectExplorer::ProjectManager::projects();
|
||||||
|
|
||||||
|
QString projectFilter;
|
||||||
|
QString fileQuery = query;
|
||||||
|
const int colonIdx = query.indexOf(':');
|
||||||
|
if (colonIdx > 0) {
|
||||||
|
const QString prefix = query.left(colonIdx);
|
||||||
|
for (auto project : allProjects) {
|
||||||
|
if (project && project->displayName().compare(prefix, Qt::CaseInsensitive) == 0) {
|
||||||
|
projectFilter = project->displayName();
|
||||||
|
fileQuery = query.mid(colonIdx + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectFilter.isEmpty() && colonIdx < 0) {
|
||||||
|
const QString lowerQ = query.toLower();
|
||||||
|
for (auto project : allProjects) {
|
||||||
|
if (!project)
|
||||||
|
continue;
|
||||||
|
const QString name = project->displayName();
|
||||||
|
if (query.isEmpty() || name.toLower().startsWith(lowerQ)) {
|
||||||
|
QVariantMap item;
|
||||||
|
item["absolutePath"] = QString();
|
||||||
|
item["relativePath"] = name;
|
||||||
|
item["projectName"] = name;
|
||||||
|
item["isProject"] = true;
|
||||||
|
results.append(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<FileResult> candidates;
|
||||||
|
const QString lowerFileQuery = fileQuery.toLower();
|
||||||
|
const bool emptyFileQuery = fileQuery.isEmpty();
|
||||||
|
|
||||||
|
for (auto project : allProjects) {
|
||||||
|
if (!project)
|
||||||
|
continue;
|
||||||
|
if (!projectFilter.isEmpty() && project->displayName() != projectFilter)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const auto projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
|
||||||
|
const QString projectDir = project->projectDirectory().path();
|
||||||
|
const QString projectName = project->displayName();
|
||||||
|
|
||||||
|
for (const auto &filePath : projectFiles) {
|
||||||
|
const QString absolutePath = filePath.path();
|
||||||
|
const QFileInfo fileInfo(absolutePath);
|
||||||
|
const QString fileName = fileInfo.fileName();
|
||||||
|
const QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
|
||||||
|
const QString lowerFileName = fileName.toLower();
|
||||||
|
const QString lowerRelativePath = relativePath.toLower();
|
||||||
|
|
||||||
|
int priority = -1;
|
||||||
|
if (emptyFileQuery) {
|
||||||
|
priority = 3;
|
||||||
|
} else if (lowerFileName == lowerFileQuery) {
|
||||||
|
priority = 0;
|
||||||
|
} else if (lowerFileName.startsWith(lowerFileQuery)) {
|
||||||
|
priority = 1;
|
||||||
|
} else if (lowerFileName.contains(lowerFileQuery)) {
|
||||||
|
priority = 2;
|
||||||
|
} else if (lowerRelativePath.contains(lowerFileQuery)) {
|
||||||
|
priority = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priority >= 0)
|
||||||
|
candidates.append({absolutePath, relativePath, projectName, priority});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(candidates.begin(), candidates.end(), [](const FileResult &a, const FileResult &b) {
|
||||||
|
if (a.priority != b.priority)
|
||||||
|
return a.priority < b.priority;
|
||||||
|
return a.relativePath < b.relativePath;
|
||||||
|
});
|
||||||
|
|
||||||
|
const int maxFiles = qMax(0, 10 - results.size());
|
||||||
|
const int count = qMin(candidates.size(), maxFiles);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
QVariantMap item;
|
||||||
|
item["absolutePath"] = candidates[i].absolutePath;
|
||||||
|
item["relativePath"] = candidates[i].relativePath;
|
||||||
|
item["projectName"] = candidates[i].projectName;
|
||||||
|
item["isProject"] = false;
|
||||||
|
results.append(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList FileMentionItem::getOpenFiles(const QString &query)
|
||||||
|
{
|
||||||
|
QVariantList results;
|
||||||
|
const QString lowerQuery = query.toLower();
|
||||||
|
const bool emptyQuery = query.isEmpty();
|
||||||
|
QSet<QString> addedPaths;
|
||||||
|
|
||||||
|
auto tryAddDocument = [&](Core::IDocument *document) {
|
||||||
|
if (!document)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QString absolutePath = document->filePath().toFSPathString();
|
||||||
|
if (absolutePath.isEmpty() || addedPaths.contains(absolutePath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QFileInfo fileInfo(absolutePath);
|
||||||
|
const QString fileName = fileInfo.fileName();
|
||||||
|
if (fileName.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QString relativePath = absolutePath;
|
||||||
|
QString projectName;
|
||||||
|
|
||||||
|
auto project = ProjectExplorer::ProjectManager::projectForFile(document->filePath());
|
||||||
|
if (project) {
|
||||||
|
projectName = project->displayName();
|
||||||
|
relativePath = QDir(project->projectDirectory().path()).relativeFilePath(absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emptyQuery) {
|
||||||
|
const QString lowerFileName = fileName.toLower();
|
||||||
|
const QString lowerRelativePath = relativePath.toLower();
|
||||||
|
if (!lowerFileName.contains(lowerQuery) && !lowerRelativePath.contains(lowerQuery))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addedPaths.insert(absolutePath);
|
||||||
|
|
||||||
|
QVariantMap item;
|
||||||
|
item["absolutePath"] = absolutePath;
|
||||||
|
item["relativePath"] = relativePath;
|
||||||
|
item["projectName"] = projectName;
|
||||||
|
item["isProject"] = false;
|
||||||
|
item["isOpen"] = true;
|
||||||
|
results.append(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (auto current = Core::EditorManager::currentEditor())
|
||||||
|
tryAddDocument(current->document());
|
||||||
|
|
||||||
|
for (auto editor : Core::EditorManager::visibleEditors())
|
||||||
|
if (editor)
|
||||||
|
tryAddDocument(editor->document());
|
||||||
|
|
||||||
|
for (auto document : Core::DocumentModel::openedDocuments())
|
||||||
|
tryAddDocument(document);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FileMentionItem::readFileLines(const QString &filePath, int startLine, int endLine)
|
||||||
|
{
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
QTextStream stream(&file);
|
||||||
|
QString result;
|
||||||
|
int lineNum = 1;
|
||||||
|
while (!stream.atEnd()) {
|
||||||
|
const QString line = stream.readLine();
|
||||||
|
if (lineNum >= startLine)
|
||||||
|
result += line + '\n';
|
||||||
|
if (lineNum >= endLine)
|
||||||
|
break;
|
||||||
|
++lineNum;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FileMentionItem::fileExtension(const QString &filePath)
|
||||||
|
{
|
||||||
|
const int dot = filePath.lastIndexOf('.');
|
||||||
|
return dot >= 0 ? filePath.mid(dot + 1) : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
84
ChatView/FileMentionItem.hpp
Normal file
84
ChatView/FileMentionItem.hpp
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QQuickItem>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QVariantList>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class FileMentionItem : public QQuickItem
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QVariantList searchResults READ searchResults NOTIFY searchResultsChanged FINAL)
|
||||||
|
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged FINAL)
|
||||||
|
|
||||||
|
QML_ELEMENT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit FileMentionItem(QQuickItem *parent = nullptr);
|
||||||
|
|
||||||
|
QVariantList searchResults() const;
|
||||||
|
int currentIndex() const;
|
||||||
|
void setCurrentIndex(int index);
|
||||||
|
|
||||||
|
Q_INVOKABLE void updateSearch(const QString &query);
|
||||||
|
Q_INVOKABLE void refreshSearch();
|
||||||
|
Q_INVOKABLE void moveUp();
|
||||||
|
Q_INVOKABLE void moveDown();
|
||||||
|
Q_INVOKABLE void selectCurrent();
|
||||||
|
Q_INVOKABLE void dismiss();
|
||||||
|
|
||||||
|
Q_INVOKABLE QVariantMap handleFileSelection(
|
||||||
|
const QString &absolutePath,
|
||||||
|
const QString &relativePath,
|
||||||
|
const QString &projectName,
|
||||||
|
const QString ¤tQuery,
|
||||||
|
bool useTools);
|
||||||
|
|
||||||
|
Q_INVOKABLE void registerMention(const QString &mentionKey, const QString &absolutePath);
|
||||||
|
Q_INVOKABLE void clearMentions();
|
||||||
|
Q_INVOKABLE QString expandMentions(const QString &text);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void searchResultsChanged();
|
||||||
|
void currentIndexChanged();
|
||||||
|
void fileSelected(const QString &absolutePath,
|
||||||
|
const QString &relativePath,
|
||||||
|
const QString &projectName);
|
||||||
|
void projectSelected(const QString &projectName);
|
||||||
|
void dismissed();
|
||||||
|
void fileAttachRequested(const QStringList &filePaths);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QVariantList searchProjectFiles(const QString &query);
|
||||||
|
QVariantList getOpenFiles(const QString &query);
|
||||||
|
QString readFileLines(const QString &filePath, int startLine, int endLine);
|
||||||
|
static QString fileExtension(const QString &filePath);
|
||||||
|
|
||||||
|
QVariantList m_searchResults;
|
||||||
|
int m_currentIndex = 0;
|
||||||
|
QString m_lastQuery;
|
||||||
|
QHash<QString, QString> m_atMentionMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -280,6 +280,10 @@ ChatRootView {
|
|||||||
messageInput.cursorPosition = model.content.length
|
messageInput.cursorPosition = model.content.length
|
||||||
root.chatModel.resetModelTo(idx)
|
root.chatModel.resetModelTo(idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onOpenFileRequested: function(filePath) {
|
||||||
|
root.openFileInEditor(filePath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -368,7 +372,38 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTextChanged: root.calculateMessageTokensCount(messageInput.text)
|
onTextChanged: {
|
||||||
|
root.calculateMessageTokensCount(messageInput.text)
|
||||||
|
var cursorPos = messageInput.cursorPosition
|
||||||
|
var textBefore = messageInput.text.substring(0, cursorPos)
|
||||||
|
var atIndex = textBefore.lastIndexOf('@')
|
||||||
|
if (atIndex >= 0) {
|
||||||
|
var query = textBefore.substring(atIndex + 1)
|
||||||
|
if (query.indexOf(' ') === -1 && query.indexOf('\n') === -1) {
|
||||||
|
fileMention.updateSearch(query)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileMention.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: function(event) {
|
||||||
|
if (fileMentionPopup.visible) {
|
||||||
|
if (event.key === Qt.Key_Down) {
|
||||||
|
fileMention.moveDown()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Up) {
|
||||||
|
fileMention.moveUp()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
fileMention.selectCurrent()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Escape) {
|
||||||
|
fileMention.dismiss()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@ -480,7 +515,7 @@ ChatRootView {
|
|||||||
sequences: ["Ctrl+Return", "Ctrl+Enter"]
|
sequences: ["Ctrl+Return", "Ctrl+Enter"]
|
||||||
context: Qt.WindowShortcut
|
context: Qt.WindowShortcut
|
||||||
onActivated: {
|
onActivated: {
|
||||||
if (messageInput.activeFocus && !Qt.inputMethod.visible) {
|
if (messageInput.activeFocus && !Qt.inputMethod.visible && !fileMentionPopup.visible) {
|
||||||
root.sendChatMessage()
|
root.sendChatMessage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -497,8 +532,9 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendChatMessage() {
|
function sendChatMessage() {
|
||||||
root.sendMessage(messageInput.text)
|
root.sendMessage(fileMention.expandMentions(messageInput.text))
|
||||||
messageInput.text = ""
|
messageInput.text = ""
|
||||||
|
fileMention.clearMentions()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -572,6 +608,93 @@ ChatRootView {
|
|||||||
infoToast.show(root.lastInfoMessage)
|
infoToast.show(root.lastInfoMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function onOpenFilesChanged() {
|
||||||
|
if (fileMentionPopup.visible)
|
||||||
|
Qt.callLater(fileMention.refreshSearch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMentionItem {
|
||||||
|
id: fileMention
|
||||||
|
|
||||||
|
onProjectSelected: function(projectName) {
|
||||||
|
var cursorPos = messageInput.cursorPosition
|
||||||
|
var text = messageInput.text
|
||||||
|
var textBefore = text.substring(0, cursorPos)
|
||||||
|
var atIndex = textBefore.lastIndexOf('@')
|
||||||
|
var mention = '@' + projectName + ':'
|
||||||
|
if (atIndex >= 0) {
|
||||||
|
var newText = text.substring(0, atIndex) + mention + text.substring(cursorPos)
|
||||||
|
messageInput.text = newText
|
||||||
|
messageInput.cursorPosition = atIndex + mention.length
|
||||||
|
}
|
||||||
|
fileMention.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileSelected: function(absolutePath, relativePath, projectName) {
|
||||||
|
var cursorPos = messageInput.cursorPosition
|
||||||
|
var text = messageInput.text
|
||||||
|
var textBefore = text.substring(0, cursorPos)
|
||||||
|
var atIndex = textBefore.lastIndexOf('@')
|
||||||
|
var currentQuery = atIndex >= 0 ? textBefore.substring(atIndex + 1) : ""
|
||||||
|
|
||||||
|
var result = fileMention.handleFileSelection(
|
||||||
|
absolutePath, relativePath, projectName, currentQuery, root.useTools)
|
||||||
|
|
||||||
|
if (result.mode === "mention") {
|
||||||
|
if (atIndex >= 0) {
|
||||||
|
let newText = text.substring(0, atIndex) + result.mentionText + text.substring(cursorPos)
|
||||||
|
messageInput.text = newText
|
||||||
|
messageInput.cursorPosition = atIndex + result.mentionText.length
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (atIndex >= 0) {
|
||||||
|
let newText = text.substring(0, atIndex) + text.substring(cursorPos)
|
||||||
|
messageInput.text = newText
|
||||||
|
messageInput.cursorPosition = atIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileMention.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileAttachRequested: function(filePaths) {
|
||||||
|
root.addFilesToAttachList(filePaths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMentionPopup {
|
||||||
|
id: fileMentionPopup
|
||||||
|
|
||||||
|
z: 999
|
||||||
|
width: Math.min(480, root.width - 20)
|
||||||
|
|
||||||
|
x: Math.max(5, Math.min(view.x + 5, root.width - width - 5))
|
||||||
|
y: view.y - height - 4
|
||||||
|
|
||||||
|
searchResults: fileMention.searchResults
|
||||||
|
|
||||||
|
onFileSelected: function(absolutePath, relativePath, projectName) {
|
||||||
|
fileMention.fileSelected(absolutePath, relativePath, projectName)
|
||||||
|
}
|
||||||
|
onProjectSelected: function(projectName) {
|
||||||
|
fileMention.projectSelected(projectName)
|
||||||
|
}
|
||||||
|
onDismissed: fileMention.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: fileMention
|
||||||
|
function onCurrentIndexChanged() {
|
||||||
|
fileMentionPopup.currentIndex = fileMention.currentIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: fileMentionPopup
|
||||||
|
function onCurrentIndexChanged() {
|
||||||
|
fileMention.currentIndex = fileMentionPopup.currentIndex
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -51,6 +51,7 @@ Rectangle {
|
|||||||
property int messageIndex: -1
|
property int messageIndex: -1
|
||||||
|
|
||||||
signal resetChatToMessage(int index)
|
signal resetChatToMessage(int index)
|
||||||
|
signal openFileRequested(string filePath)
|
||||||
|
|
||||||
height: msgColumn.implicitHeight + 10
|
height: msgColumn.implicitHeight + 10
|
||||||
radius: 8
|
radius: 8
|
||||||
@ -204,6 +205,15 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLinkActivated: function(link) {
|
||||||
|
if (link.startsWith("file://")) {
|
||||||
|
var filePath = link.replace(/^file:\/\//, "")
|
||||||
|
root.openFileRequested(filePath)
|
||||||
|
} else {
|
||||||
|
Qt.openUrlExternally(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ChatUtils {
|
ChatUtils {
|
||||||
id: utils
|
id: utils
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -29,8 +29,6 @@ TextEdit {
|
|||||||
selectionColor: palette.highlight
|
selectionColor: palette.highlight
|
||||||
color: palette.text
|
color: palette.text
|
||||||
|
|
||||||
onLinkActivated: (link) => Qt.openUrlExternally(link)
|
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
|
|||||||
184
ChatView/qml/controls/FileMentionPopup.qml
Normal file
184
ChatView/qml/controls/FileMentionPopup.qml
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var searchResults: []
|
||||||
|
property int currentIndex: 0
|
||||||
|
|
||||||
|
signal fileSelected(string absolutePath, string relativePath, string projectName)
|
||||||
|
signal projectSelected(string projectName)
|
||||||
|
signal dismissed()
|
||||||
|
|
||||||
|
visible: searchResults.length > 0
|
||||||
|
height: Math.min(searchResults.length * 36, 36 * 6) + 2
|
||||||
|
color: palette.window
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
radius: 4
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: listView
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 1
|
||||||
|
model: root.searchResults
|
||||||
|
currentIndex: root.currentIndex
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
policy: ScrollBar.AsNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: delegateItem
|
||||||
|
|
||||||
|
required property int index
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
readonly property bool isProject: modelData.isProject === true
|
||||||
|
readonly property bool isOpen: modelData.isOpen === true
|
||||||
|
readonly property string fileName: {
|
||||||
|
if (isProject)
|
||||||
|
return modelData.projectName
|
||||||
|
const parts = modelData.relativePath.split('/')
|
||||||
|
return parts[parts.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
width: listView.width
|
||||||
|
height: 36
|
||||||
|
color: index === root.currentIndex
|
||||||
|
? palette.highlight
|
||||||
|
: (hoverArea.containsMouse
|
||||||
|
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.25)
|
||||||
|
: "transparent")
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.preferredWidth: 18
|
||||||
|
Layout.preferredHeight: 18
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 3
|
||||||
|
visible: delegateItem.isProject || delegateItem.isOpen
|
||||||
|
|
||||||
|
color: {
|
||||||
|
if (delegateItem.index === root.currentIndex)
|
||||||
|
return Qt.rgba(palette.highlightedText.r,
|
||||||
|
palette.highlightedText.g,
|
||||||
|
palette.highlightedText.b, 0.2)
|
||||||
|
if (delegateItem.isProject)
|
||||||
|
return Qt.rgba(palette.highlight.r,
|
||||||
|
palette.highlight.g,
|
||||||
|
palette.highlight.b, 0.3)
|
||||||
|
return Qt.rgba(0.2, 0.7, 0.4, 0.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: delegateItem.isProject ? "P" : "O"
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: {
|
||||||
|
if (delegateItem.index === root.currentIndex)
|
||||||
|
return palette.highlightedText
|
||||||
|
if (delegateItem.isProject)
|
||||||
|
return palette.highlight
|
||||||
|
return Qt.rgba(0.1, 0.6, 0.3, 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.preferredWidth: 160
|
||||||
|
text: delegateItem.fileName
|
||||||
|
color: delegateItem.index === root.currentIndex
|
||||||
|
? palette.highlightedText
|
||||||
|
: (delegateItem.isProject ? palette.highlight : palette.text)
|
||||||
|
font.bold: true
|
||||||
|
font.italic: delegateItem.isProject
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: delegateItem.isProject
|
||||||
|
? "→"
|
||||||
|
: (delegateItem.modelData.projectName + " / " + delegateItem.modelData.relativePath)
|
||||||
|
color: delegateItem.index === root.currentIndex
|
||||||
|
? (delegateItem.isProject
|
||||||
|
? palette.highlightedText
|
||||||
|
: Qt.rgba(palette.highlightedText.r,
|
||||||
|
palette.highlightedText.g,
|
||||||
|
palette.highlightedText.b, 0.7))
|
||||||
|
: palette.mid
|
||||||
|
font.pixelSize: delegateItem.isProject ? 12 : 11
|
||||||
|
elide: Text.ElideLeft
|
||||||
|
horizontalAlignment: delegateItem.isProject ? Text.AlignLeft : Text.AlignRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: hoverArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: handleSelection(delegateItem.modelData)
|
||||||
|
onEntered: root.currentIndex = delegateItem.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelection(item) {
|
||||||
|
if (item.isProject === true) {
|
||||||
|
root.projectSelected(item.projectName)
|
||||||
|
} else {
|
||||||
|
root.fileSelected(item.absolutePath, item.relativePath, item.projectName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCurrent() {
|
||||||
|
if (currentIndex >= 0 && currentIndex < searchResults.length)
|
||||||
|
handleSelection(searchResults[currentIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveDown() {
|
||||||
|
if (currentIndex < searchResults.length - 1)
|
||||||
|
currentIndex++
|
||||||
|
listView.positionViewAtIndex(currentIndex, ListView.Contain)
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveUp() {
|
||||||
|
if (currentIndex > 0)
|
||||||
|
currentIndex--
|
||||||
|
listView.positionViewAtIndex(currentIndex, ListView.Contain)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user