diff --git a/settings/GeneralSettings.cpp b/settings/GeneralSettings.cpp index cfe926a..11f0133 100644 --- a/settings/GeneralSettings.cpp +++ b/settings/GeneralSettings.cpp @@ -226,6 +226,12 @@ GeneralSettings::GeneralSettings() Tr::tr("Allow tools to write and modify files on disk (WARNING: Use with caution!)")); allowFileSystemWrite.setDefaultValue(false); + allowReadOutsideProject.setSettingsKey(Constants::CA_ALLOW_READ_OUTSIDE_PROJECT); + allowReadOutsideProject.setLabelText(Tr::tr("Allow reading files outside project")); + allowReadOutsideProject.setToolTip( + Tr::tr("Allow tools to read files outside the project scope (system headers, Qt files, external libraries)")); + allowReadOutsideProject.setDefaultValue(true); + autoApplyFileEdits.setSettingsKey(Constants::CA_AUTO_APPLY_FILE_EDITS); autoApplyFileEdits.setLabelText(Tr::tr("Automatically apply file edits")); autoApplyFileEdits.setToolTip( @@ -279,7 +285,7 @@ GeneralSettings::GeneralSettings() auto caGroup = Group{ title(TrConstants::CHAT_ASSISTANT), Column{caGrid, - Column{useTools, allowFileSystemRead, allowFileSystemWrite, autoApplyFileEdits}, + Column{useTools, allowFileSystemRead, allowFileSystemWrite, allowReadOutsideProject, autoApplyFileEdits}, caTemplateDescription}}; auto rootLayout = Column{ @@ -527,6 +533,7 @@ void GeneralSettings::resetPageToDefaults() resetAspect(useTools); resetAspect(allowFileSystemRead); resetAspect(allowFileSystemWrite); + resetAspect(allowReadOutsideProject); resetAspect(autoApplyFileEdits); writeSettings(); } diff --git a/settings/GeneralSettings.hpp b/settings/GeneralSettings.hpp index e84b6ef..406f01a 100644 --- a/settings/GeneralSettings.hpp +++ b/settings/GeneralSettings.hpp @@ -103,6 +103,7 @@ public: Utils::BoolAspect useTools{this}; Utils::BoolAspect allowFileSystemRead{this}; Utils::BoolAspect allowFileSystemWrite{this}; + Utils::BoolAspect allowReadOutsideProject{this}; Utils::BoolAspect autoApplyFileEdits{this}; Utils::StringAspect caTemplateDescription{this}; diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp index ccffc49..5fa7e64 100644 --- a/settings/SettingsConstants.hpp +++ b/settings/SettingsConstants.hpp @@ -88,6 +88,7 @@ const char CA_ENABLE_CHAT_IN_NAVIGATION_PANEL[] = "QodeAssist.caEnableChatInNavi const char CA_USE_TOOLS[] = "QodeAssist.caUseTools"; const char CA_ALLOW_FILE_SYSTEM_READ[] = "QodeAssist.caAllowFileSystemRead"; const char CA_ALLOW_FILE_SYSTEM_WRITE[] = "QodeAssist.caAllowFileSystemWrite"; +const char CA_ALLOW_READ_OUTSIDE_PROJECT[] = "QodeAssist.caAllowReadOutsideProject"; const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions"; const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId"; diff --git a/tools/FindFileTool.cpp b/tools/FindFileTool.cpp index 1165477..0ba80f9 100644 --- a/tools/FindFileTool.cpp +++ b/tools/FindFileTool.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -48,6 +49,7 @@ QString FindFileTool::stringName() const QString FindFileTool::description() const { return "Search for files in the project by filename, partial name, or path. " + "Searches both in CMake-registered files and filesystem (finds .gitignore, Python scripts, README, etc.). " "Supports exact/partial filename match, relative/absolute paths, file extension filtering, " "and case-insensitive search. " "Returns matching files with absolute and relative paths."; @@ -60,7 +62,9 @@ QJsonObject FindFileTool::getDefinition(LLMCore::ToolSchemaFormat format) const QJsonObject queryProperty; queryProperty["type"] = "string"; queryProperty["description"] - = "The filename, partial filename, or path to search for (case-insensitive)"; + = "The filename, partial filename, or path to search for (case-insensitive). " + "Finds ALL files in project directory including .gitignore, README.md, Python scripts, " + "config files, etc., even if not in CMake build system"; properties["query"] = queryProperty; QJsonObject filePatternProperty; @@ -127,22 +131,30 @@ QFuture FindFileTool::executeAsync(const QJsonObject &input) QFileInfo queryInfo(query); if (queryInfo.isAbsolute() && queryInfo.exists() && queryInfo.isFile()) { QString canonicalPath = queryInfo.canonicalFilePath(); - if (!isFileInProject(canonicalPath)) { - QString error = QString("Error: File '%1' exists but is outside the project scope. " - "Only files within the project can be accessed.") - .arg(canonicalPath); - throw std::runtime_error(error.toStdString()); + bool isInProject = isFileInProject(canonicalPath); + + // Check if reading outside project is allowed + if (!isInProject) { + const auto &settings = Settings::generalSettings(); + if (!settings.allowReadOutsideProject()) { + QString error = QString("Error: File '%1' exists but is outside the project scope. " + "Enable 'Allow reading files outside project' in settings to access this file.") + .arg(canonicalPath); + throw std::runtime_error(error.toStdString()); + } + LOG_MESSAGE(QString("Finding file outside project scope: %1").arg(canonicalPath)); } - auto project = ProjectExplorer::ProjectManager::projectForFile( - Utils::FilePath::fromString(canonicalPath)); + auto project = isInProject ? ProjectExplorer::ProjectManager::projectForFile( + Utils::FilePath::fromString(canonicalPath)) : nullptr; - if (project && !m_ignoreManager->shouldIgnore(canonicalPath, project)) { + if (!isInProject || (project && !m_ignoreManager->shouldIgnore(canonicalPath, project))) { FileMatch match; match.absolutePath = canonicalPath; - match.relativePath = QDir(project->projectDirectory().toFSPathString()) - .relativeFilePath(canonicalPath); - match.projectName = project->displayName(); + match.relativePath = isInProject && project + ? QDir(project->projectDirectory().toFSPathString()).relativeFilePath(canonicalPath) + : canonicalPath; + match.projectName = isInProject && project ? project->displayName() : "External"; match.matchType = FileMatch::PathMatch; QList matches; @@ -235,11 +247,125 @@ QList FindFileTool::findMatchingFiles(const QString &qu } } + // If we didn't find enough matches in project files, search the filesystem + if (matches.size() < maxResults) { + LOG_MESSAGE(QString("FindFileTool: Extending search to filesystem (found %1 matches so far)") + .arg(matches.size())); + + for (auto project : projects) { + if (!project) + continue; + + if (matches.size() >= maxResults) { + break; + } + + Utils::FilePath projectDir = project->projectDirectory(); + QString projectName = project->displayName(); + QString projectDirStr = projectDir.toFSPathString(); + + int depth = 0; + searchInFileSystem(projectDirStr, lowerQuery, projectName, projectDirStr, + project, matches, maxResults, depth); + } + } + std::sort(matches.begin(), matches.end()); return matches; } +void FindFileTool::searchInFileSystem(const QString &dirPath, + const QString &query, + const QString &projectName, + const QString &projectDir, + ProjectExplorer::Project *project, + QList &matches, + int maxResults, + int ¤tDepth, + int maxDepth) const +{ + if (currentDepth > maxDepth || matches.size() >= maxResults) { + return; + } + + currentDepth++; + + QDir dir(dirPath); + if (!dir.exists()) { + currentDepth--; + return; + } + + // Get all entries (files and directories) + QFileInfoList entries = dir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden); + + for (const QFileInfo &entry : entries) { + if (matches.size() >= maxResults) { + break; + } + + QString absolutePath = entry.absoluteFilePath(); + + // Check if should be ignored + if (project && m_ignoreManager->shouldIgnore(absolutePath, project)) { + continue; + } + + // Skip common build/cache directories + QString fileName = entry.fileName(); + if (entry.isDir()) { + // Skip common build/cache directories + if (fileName == "build" || fileName == ".git" || fileName == "node_modules" || + fileName == "__pycache__" || fileName == ".venv" || fileName == "venv" || + fileName == ".cmake" || fileName == "CMakeFiles" || fileName.startsWith(".qt")) { + continue; + } + + // Recurse into subdirectory + searchInFileSystem(absolutePath, query, projectName, projectDir, + project, matches, maxResults, currentDepth, maxDepth); + continue; + } + + // Check if already in matches (avoid duplicates from project files) + bool alreadyAdded = false; + for (const auto &match : matches) { + if (match.absolutePath == absolutePath) { + alreadyAdded = true; + break; + } + } + + if (alreadyAdded) { + continue; + } + + // Match logic + QString lowerFileName = fileName.toLower(); + QString relativePath = QDir(projectDir).relativeFilePath(absolutePath); + QString lowerRelativePath = relativePath.toLower(); + + FileMatch match; + match.absolutePath = absolutePath; + match.relativePath = relativePath; + match.projectName = projectName; + + if (lowerFileName == query) { + match.matchType = FileMatch::ExactName; + matches.append(match); + } else if (lowerRelativePath.contains(query)) { + match.matchType = FileMatch::PathMatch; + matches.append(match); + } else if (lowerFileName.contains(query)) { + match.matchType = FileMatch::PartialName; + matches.append(match); + } + } + + currentDepth--; +} + bool FindFileTool::matchesFilePattern(const QString &fileName, const QString &pattern) const { if (pattern.isEmpty()) { diff --git a/tools/FindFileTool.hpp b/tools/FindFileTool.hpp index db6af26..0398bbb 100644 --- a/tools/FindFileTool.hpp +++ b/tools/FindFileTool.hpp @@ -56,6 +56,15 @@ private: }; QList findMatchingFiles(const QString &query, int maxResults) const; + void searchInFileSystem(const QString &dirPath, + const QString &query, + const QString &projectName, + const QString &projectDir, + ProjectExplorer::Project *project, + QList &matches, + int maxResults, + int ¤tDepth, + int maxDepth = 10) const; QString formatResults(const QList &matches, int totalFound, int maxResults) const; bool isFileInProject(const QString &filePath) const; bool matchesFilePattern(const QString &fileName, const QString &pattern) const; diff --git a/tools/ReadFileByPathTool.cpp b/tools/ReadFileByPathTool.cpp index 71c6169..8f3555e 100644 --- a/tools/ReadFileByPathTool.cpp +++ b/tools/ReadFileByPathTool.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -107,14 +108,23 @@ QFuture ReadProjectFileByPathTool::executeAsync(const QJsonObject &inpu QString canonicalPath = fileInfo.canonicalFilePath(); - if (!isFileInProject(canonicalPath)) { - QString error = QString("Error: File '%1' is not part of the project").arg(filePath); - throw std::runtime_error(error.toStdString()); + bool isInProject = isFileInProject(canonicalPath); + + // Check if reading outside project is allowed + if (!isInProject) { + const auto &settings = Settings::generalSettings(); + if (!settings.allowReadOutsideProject()) { + QString error = QString("Error: File '%1' is not part of the project. " + "Enable 'Allow reading files outside project' in settings to access this file.") + .arg(filePath); + throw std::runtime_error(error.toStdString()); + } + LOG_MESSAGE(QString("Reading file outside project scope: %1").arg(canonicalPath)); } - auto project = ProjectExplorer::ProjectManager::projectForFile( - Utils::FilePath::fromString(canonicalPath)); - if (project && m_ignoreManager->shouldIgnore(canonicalPath, project)) { + auto project = isInProject ? ProjectExplorer::ProjectManager::projectForFile( + Utils::FilePath::fromString(canonicalPath)) : nullptr; + if (isInProject && project && m_ignoreManager->shouldIgnore(canonicalPath, project)) { QString error = QString("Error: File '%1' is excluded by .qodeassistignore").arg(filePath); throw std::runtime_error(error.toStdString());