From 350a49cef9b46374ebfdde724121058c6560d5ca Mon Sep 17 00:00:00 2001 From: travertexg Date: Mon, 9 Jun 2025 09:40:16 +0000 Subject: [PATCH 1/8] feat: Add Omnisearch support for regex search This commit introduces the option to use Omnisearch as a backend for the regex search functionality, in addition to the existing ripgrep backend. --- src/components/chat-view/ChatView.tsx | 13 +- src/core/regex/omnisearch-index.ts | 140 ++++++++++++++++++ src/core/regex/regex-common.ts | 63 ++++++++ .../index.ts => regex/ripgrep-index.ts} | 70 +-------- src/lang/locale/en.ts | 6 +- src/lang/locale/zh-cn.ts | 6 +- src/settings/SettingTab.tsx | 15 ++ src/types/settings.ts | 1 + 8 files changed, 246 insertions(+), 68 deletions(-) create mode 100644 src/core/regex/omnisearch-index.ts create mode 100644 src/core/regex/regex-common.ts rename src/core/{ripgrep/index.ts => regex/ripgrep-index.ts} (68%) diff --git a/src/components/chat-view/ChatView.tsx b/src/components/chat-view/ChatView.tsx index 7a2c762..5035a91 100644 --- a/src/components/chat-view/ChatView.tsx +++ b/src/components/chat-view/ChatView.tsx @@ -29,7 +29,8 @@ import { LLMBaseUrlNotSetException, LLMModelNotSetException, } from '../../core/llm/exception' -import { regexSearchFiles } from '../../core/ripgrep' +import { regexSearchFilesWithRipgrep } from '../../core/regex/ripgrep-index' +import { regexSearchFilesWithOmnisearch } from '../../core/regex/omnisearch-index' import { useChatHistory } from '../../hooks/use-chat-history' import { useCustomModes } from '../../hooks/use-custom-mode' import { t } from '../../lang/helpers' @@ -609,10 +610,16 @@ const Chat = forwardRef((props, ref) => { } } else if (toolArgs.type === 'regex_search_files') { // @ts-expect-error Obsidian API type mismatch + const searchBackend = settings.regexSearchBackend const baseVaultPath = String(app.vault.adapter.getBasePath()) - const ripgrepPath = settings.ripgrepPath const absolutePath = path.join(baseVaultPath, toolArgs.filepath) - const results = await regexSearchFiles(absolutePath, toolArgs.regex, ripgrepPath) + let results: string; + if (searchBackend === 'omnisearch') { + results = await regexSearchFilesWithOmnisearch(absolutePath, toolArgs.regex, app) + } else { + const ripgrepPath = settings.ripgrepPath + results = await regexSearchFilesWithRipgrep(absolutePath, toolArgs.regex, ripgrepPath) + } const formattedContent = `[regex_search_files for '${toolArgs.filepath}'] Result:\n${results}\n`; return { type: 'regex_search_files', diff --git a/src/core/regex/omnisearch-index.ts b/src/core/regex/omnisearch-index.ts new file mode 100644 index 0000000..15322c5 --- /dev/null +++ b/src/core/regex/omnisearch-index.ts @@ -0,0 +1,140 @@ +import { App } from "obsidian"; +import { + MAX_RESULTS, + truncateLine, + SearchResult, + formatResults, +} from './regex-common'; + +// --- Omnisearch API and Helper Types --- + +type SearchMatchApi = { + match: string; + offset: number; +}; + +type ResultNoteApi = { + score: number; + vault: string; + path: string; + basename: string; + foundWords: string[]; + matches: SearchMatchApi[]; + excerpt: string; +}; + +type OmnisearchApi = { + search: (query: string) => Promise; + // ... other API methods +}; + +declare global { + interface Window { + omnisearch: OmnisearchApi; + } +} + +/** + * Checks if the Omnisearch plugin's API is available. + * @returns {boolean} True if the API is ready, false otherwise. + */ +function isOmnisearchAvailable(): boolean { + return window.omnisearch && typeof window.omnisearch.search === "function"; +} + +/** + * Finds the line number, column number, and content for a given character offset in a file. + * @param allLines All lines in the file. + * @param offset The character offset of the match. + * @returns An object with line number, column number, and the full line content. + */ +function findLineAndColumnFromOffset( + allLines: string[], + offset: number +): { lineNumber: number; columnNumber: number; lineContent: string } { + let charCount = 0; + for (let i = 0; i < allLines.length; i++) { + const line = allLines[i]; + // The line ending length (1 for \n, 2 for \r\n) can vary. + // A simple +1 is a reasonable approximation for this calculation. + const lineEndOffset = charCount + line.length + 1; + + if (offset < lineEndOffset) { + const columnNumber = offset - charCount; + return { lineNumber: i, columnNumber, lineContent: line }; + } + charCount = lineEndOffset; + } + return { lineNumber: -1, columnNumber: -1, lineContent: "" }; +} + +/** + * Searches using Omnisearch and builds context for each match to replicate ripgrep's output. + * @param vaultPath The absolute path of the vault for making relative paths. + * @param query The search query for Omnisearch. Note: Omnisearch does not support full regex. + * @param app The Obsidian App instance. + * @returns A formatted string of search results. + */ +export async function regexSearchFilesWithOmnisearch( + vaultPath: string, + query: string, + app: App, +): Promise { + try { + if (!isOmnisearchAvailable()) { + throw new Error( + "Omnisearch plugin not found or not active. Please install and enable it to use this search feature." + ); + } + + // Omnisearch is not a regex engine. The function name is kept for consistency + // but the `query` will be treated as a keyword/fuzzy search by the plugin. + const apiResults = await window.omnisearch.search(query); + if (!apiResults || apiResults.length === 0) { + throw new Error("No results found."); + } + + const results: SearchResult[] = []; + + for (const result of apiResults) { + if (results.length >= MAX_RESULTS) { + break; // Stop processing new files if we have enough results + } + if (!result.matches || result.matches.length === 0) continue; + + const fileContent = await app.vault.adapter.read(result.path); + const allLines = fileContent.split("\n"); + + for (const match of result.matches) { + if (results.length >= MAX_RESULTS) { + break; // Stop processing matches if we have enough results + } + + const { lineNumber, columnNumber, lineContent } = findLineAndColumnFromOffset( + allLines, + match.offset + ); + + if (lineNumber === -1) continue; + + const searchResult: SearchResult = { + file: result.path, + line: lineNumber + 1, // ripgrep is 1-based, so we adjust + column: columnNumber + 1, + match: truncateLine(lineContent.trimEnd()), + beforeContext: lineNumber > 0 ? [truncateLine(allLines[lineNumber - 1].trimEnd())] : [], + afterContext: + lineNumber < allLines.length - 1 + ? [truncateLine(allLines[lineNumber + 1].trimEnd())] + : [], + }; + results.push(searchResult); + } + } + + return formatResults(results, vaultPath); + } catch (error) { + console.error("Error during Omnisearch processing:", error); + return "An error occurred during the search."; + } +} \ No newline at end of file diff --git a/src/core/regex/regex-common.ts b/src/core/regex/regex-common.ts new file mode 100644 index 0000000..0e5b3e7 --- /dev/null +++ b/src/core/regex/regex-common.ts @@ -0,0 +1,63 @@ +import * as path from "path" + +// Constants +export const MAX_RESULTS = 300 +export const MAX_LINE_LENGTH = 500 + +/** + * Truncates a line if it exceeds the maximum length + * @param line The line to truncate + * @param maxLength The maximum allowed length (defaults to MAX_LINE_LENGTH) + * @returns The truncated line, or the original line if it's shorter than maxLength + */ +export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): string { + return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line +} + +export interface SearchResult { + file: string + line: number + column?: number + match?: string + beforeContext: string[] + afterContext: string[] +} + +export function formatResults(results: SearchResult[], cwd: string): string { + const groupedResults: { [key: string]: SearchResult[] } = {} + + let output = "" + if (results.length >= MAX_RESULTS) { + output += `Showing first ${MAX_RESULTS} of ${MAX_RESULTS}+ results. Use a more specific search if necessary.\n\n` + } else { + output += `Found ${results.length === 1 ? "1 result" : `${results.length.toLocaleString()} results`}.\n\n` + } + + // Group results by file name + results.slice(0, MAX_RESULTS).forEach((result) => { + const relativeFilePath = path.relative(cwd, result.file) + if (!groupedResults[relativeFilePath]) { + groupedResults[relativeFilePath] = [] + } + groupedResults[relativeFilePath].push(result) + }) + + for (const [filePath, fileResults] of Object.entries(groupedResults)) { + output += `${filePath.toPosix()}\n│----\n` + + fileResults.forEach((result, index) => { + const allLines = [...result.beforeContext, result.match, ...result.afterContext] + allLines.forEach((line) => { + output += `│${line?.trimEnd() ?? ""}\n` + }) + + if (index < fileResults.length - 1) { + output += "│----\n" + } + }) + + output += "│----\n\n" + } + + return output.trim() +} \ No newline at end of file diff --git a/src/core/ripgrep/index.ts b/src/core/regex/ripgrep-index.ts similarity index 68% rename from src/core/ripgrep/index.ts rename to src/core/regex/ripgrep-index.ts index 1572c89..eb972dc 100644 --- a/src/core/ripgrep/index.ts +++ b/src/core/regex/ripgrep-index.ts @@ -3,33 +3,16 @@ import * as childProcess from "child_process" import * as fs from "fs" import * as path from "path" import * as readline from "readline" +import { + MAX_RESULTS, + truncateLine, + SearchResult, + formatResults +} from './regex-common' const isWindows = /^win/.test(process.platform) const binName = isWindows ? "rg.exe" : "rg" -interface SearchResult { - file: string - line: number - column: number - match: string - beforeContext: string[] - afterContext: string[] -} - -// Constants -const MAX_RESULTS = 300 -const MAX_LINE_LENGTH = 500 - -/** - * Truncates a line if it exceeds the maximum length - * @param line The line to truncate - * @param maxLength The maximum allowed length (defaults to MAX_LINE_LENGTH) - * @returns The truncated line, or the original line if it's shorter than maxLength - */ -export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): string { - return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line -} - async function getBinPath(ripgrepPath: string): Promise { const binPath = path.join(ripgrepPath, binName) return (await pathExists(binPath)) ? binPath : undefined @@ -83,7 +66,7 @@ async function execRipgrep(bin: string, args: string[]): Promise { }) } -export async function regexSearchFiles( +export async function regexSearchFilesWithRipgrep( directoryPath: string, regex: string, ripgrepPath: string, @@ -162,42 +145,3 @@ export async function regexSearchFiles( return formatResults(results, directoryPath) } - -function formatResults(results: SearchResult[], cwd: string): string { - const groupedResults: { [key: string]: SearchResult[] } = {} - - let output = "" - if (results.length >= MAX_RESULTS) { - output += `Showing first ${MAX_RESULTS} of ${MAX_RESULTS}+ results. Use a more specific search if necessary.\n\n` - } else { - output += `Found ${results.length === 1 ? "1 result" : `${results.length.toLocaleString()} results`}.\n\n` - } - - // Group results by file name - results.slice(0, MAX_RESULTS).forEach((result) => { - const relativeFilePath = path.relative(cwd, result.file) - if (!groupedResults[relativeFilePath]) { - groupedResults[relativeFilePath] = [] - } - groupedResults[relativeFilePath].push(result) - }) - - for (const [filePath, fileResults] of Object.entries(groupedResults)) { - output += `${filePath.toPosix()}\n│----\n` - - fileResults.forEach((result, index) => { - const allLines = [...result.beforeContext, result.match, ...result.afterContext] - allLines.forEach((line) => { - output += `│${line?.trimEnd() ?? ""}\n` - }) - - if (index < fileResults.length - 1) { - output += "│----\n" - } - }) - - output += "│----\n\n" - } - - return output.trim() -} diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index e339ae8..868ecc0 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -227,8 +227,12 @@ export default { auto: 'Auto', semantic: 'Semantic', regex: 'Regex', + regexBackend: 'Regex search backend', + regexBackendDescription: 'Choose the backend for regex search method.', + ripgrep: 'ripgrep', + omnisearch: 'Omnisearch', ripgrepPath: 'ripgrep path', - ripgrepPathDescription: 'Path to the ripgrep binary. When using regex search, this is required.', + ripgrepPathDescription: 'Path to the ripgrep binary. When using ripgrep regex search, this is required.', }, // Chat Behavior Section diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index bd30cdb..8fe4c84 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -228,8 +228,12 @@ export default { auto: '自动', semantic: '语义', regex: '正则', + regexBackend: '正则搜索后端', + regexBackendDescription: '选择正则搜索的后端。', + ripgrep: 'ripgrep', + omnisearch: 'Omnisearch', ripgrepPath: 'ripgrep 路径', - ripgrepPathDescription: 'ripgrep 二进制文件的路径。使用正则搜索时需要此项。', + ripgrepPathDescription: 'ripgrep 二进制文件的路径。使用 ripgrep 正则搜索时需要此项。', }, // 聊天行为部分 diff --git a/src/settings/SettingTab.tsx b/src/settings/SettingTab.tsx index 2500749..fba345e 100644 --- a/src/settings/SettingTab.tsx +++ b/src/settings/SettingTab.tsx @@ -163,6 +163,21 @@ export class InfioSettingTab extends PluginSettingTab { }) }), ) + new Setting(containerEl) + .setName(t('settings.FilesSearch.regexBackend')) + .setDesc(t('settings.FilesSearch.regexBackendDescription')) + .addDropdown((dropdown) => + dropdown + .addOption('ripgrep', t('settings.FilesSearch.ripgrep')) + .addOption('omnisearch', t('settings.FilesSearch.omnisearch')) + .setValue(this.plugin.settings.regexSearchBackend) + .onChange(async (value) => { + await this.plugin.setSettings({ + ...this.plugin.settings, + regexSearchBackend: value as 'ripgrep' | 'omnisearch', + }) + }), + ) new Setting(containerEl) .setName(t('settings.FilesSearch.ripgrepPath')) .setDesc(t('settings.FilesSearch.ripgrepPathDescription')) diff --git a/src/types/settings.ts b/src/types/settings.ts index 352de1a..a67ae9f 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -261,6 +261,7 @@ export const InfioSettingsSchema = z.object({ // Files Search filesSearchMethod: z.enum(['regex', 'semantic', 'auto']).catch('auto'), + regexSearchBackend: z.enum(['omnisearch', 'ripgrep']).catch('ripgrep'), ripgrepPath: z.string().catch(''), /// [compatible] From 9984527e85d8df68e01f88314f7c689d53db63e5 Mon Sep 17 00:00:00 2001 From: travertexg Date: Mon, 9 Jun 2025 15:15:16 +0000 Subject: [PATCH 2/8] feat: Enhance file search with core plugin and Omnisearch integration - Introduces a new match_search_files tool for fuzzy/keyword search, integrating with Obsidian's core search plugin and updating Omnisearch integration for improved file search capabilities. - Adds settings for selecting search backends (core plugin, Omnisearch, ripgrep) for both regex and match searches. - Updates language files, prompts, and types to support the new functionality. - Restructures search-related files for better organization. --- src/components/chat-view/ChatView.tsx | 40 +++++++-- .../MarkdownMatchSearchFilesBlock.tsx | 52 ++++++++++++ src/components/chat-view/ReactMarkdown.tsx | 10 +++ src/core/prompts/sections/capabilities.ts | 22 +++-- src/core/prompts/sections/rules.ts | 4 +- src/core/prompts/tools/search-files.ts | 24 +++++- src/core/search/match/coreplugin-match.ts | 85 +++++++++++++++++++ .../match/omnisearch-match.ts} | 16 ++-- src/core/search/regex/coreplugin-regex.ts | 17 ++++ .../regex/ripgrep-regex.ts} | 2 +- .../search-common.ts} | 0 src/lang/locale/en.ts | 6 +- src/lang/locale/zh-cn.ts | 6 +- src/settings/SettingTab.tsx | 24 +++++- src/types/apply.ts | 10 ++- src/types/settings.ts | 5 +- src/utils/glob-utils.ts | 4 + src/utils/parse-infio-block.ts | 35 ++++++++ 18 files changed, 326 insertions(+), 36 deletions(-) create mode 100644 src/components/chat-view/Markdown/MarkdownMatchSearchFilesBlock.tsx create mode 100644 src/core/search/match/coreplugin-match.ts rename src/core/{regex/omnisearch-index.ts => search/match/omnisearch-match.ts} (87%) create mode 100644 src/core/search/regex/coreplugin-regex.ts rename src/core/{regex/ripgrep-index.ts => search/regex/ripgrep-regex.ts} (99%) rename src/core/{regex/regex-common.ts => search/search-common.ts} (100%) diff --git a/src/components/chat-view/ChatView.tsx b/src/components/chat-view/ChatView.tsx index 5035a91..a477843 100644 --- a/src/components/chat-view/ChatView.tsx +++ b/src/components/chat-view/ChatView.tsx @@ -29,8 +29,10 @@ import { LLMBaseUrlNotSetException, LLMModelNotSetException, } from '../../core/llm/exception' -import { regexSearchFilesWithRipgrep } from '../../core/regex/ripgrep-index' -import { regexSearchFilesWithOmnisearch } from '../../core/regex/omnisearch-index' +import { searchFilesWithCorePlugin } from '../../core/search/match/coreplugin-match' +import { searchFilesWithOmnisearch } from '../../core/search/match/omnisearch-match' +import { regexSearchFilesWithRipgrep } from '../../core/search/regex/ripgrep-regex' +import { regexSearchFilesWithCorePlugin } from '../../core/search/regex/coreplugin-regex' import { useChatHistory } from '../../hooks/use-chat-history' import { useCustomModes } from '../../hooks/use-custom-mode' import { t } from '../../lang/helpers' @@ -608,15 +610,37 @@ const Chat = forwardRef((props, ref) => { mentionables: [], } } - } else if (toolArgs.type === 'regex_search_files') { - // @ts-expect-error Obsidian API type mismatch - const searchBackend = settings.regexSearchBackend - const baseVaultPath = String(app.vault.adapter.getBasePath()) - const absolutePath = path.join(baseVaultPath, toolArgs.filepath) + } else if (toolArgs.type === 'match_search_files') { + const searchBackend = settings.matchSearchBackend let results: string; if (searchBackend === 'omnisearch') { - results = await regexSearchFilesWithOmnisearch(absolutePath, toolArgs.regex, app) + results = await searchFilesWithOmnisearch(toolArgs.query, app) } else { + results = await searchFilesWithCorePlugin(toolArgs.query, app) + } + const formattedContent = `[match_search_files for '${toolArgs.filepath}'] Result:\n${results}\n`; + return { + type: 'match_search_files', + applyMsgId, + applyStatus: ApplyStatus.Applied, + returnMsg: { + role: 'user', + applyStatus: ApplyStatus.Idle, + content: null, + promptContent: formattedContent, + id: uuidv4(), + mentionables: [], + } + } + } else if (toolArgs.type === 'regex_search_files') { + const searchBackend = settings.regexSearchBackend + let results: string; + if (searchBackend === 'coreplugin') { + results = await regexSearchFilesWithCorePlugin(toolArgs.regex, app) + } else { + // @ts-expect-error Obsidian API type mismatch + const baseVaultPath = String(app.vault.adapter.getBasePath()) + const absolutePath = path.join(baseVaultPath, toolArgs.filepath) const ripgrepPath = settings.ripgrepPath results = await regexSearchFilesWithRipgrep(absolutePath, toolArgs.regex, ripgrepPath) } diff --git a/src/components/chat-view/Markdown/MarkdownMatchSearchFilesBlock.tsx b/src/components/chat-view/Markdown/MarkdownMatchSearchFilesBlock.tsx new file mode 100644 index 0000000..757a3d8 --- /dev/null +++ b/src/components/chat-view/Markdown/MarkdownMatchSearchFilesBlock.tsx @@ -0,0 +1,52 @@ +import { FileSearch } from 'lucide-react' +import React from 'react' + +import { useApp } from "../../../contexts/AppContext" +import { t } from '../../../lang/helpers' +import { ApplyStatus, MatchSearchFilesToolArgs } from "../../../types/apply" +import { openMarkdownFile } from "../../../utils/obsidian" + +export default function MarkdownMatchSearchFilesBlock({ + applyStatus, + onApply, + path, + query, + finish +}: { + applyStatus: ApplyStatus + onApply: (args: MatchSearchFilesToolArgs) => void + path: string, + query: string, + finish: boolean +}) { + const app = useApp() + + const handleClick = () => { + openMarkdownFile(app, path) + } + + React.useEffect(() => { + if (finish && applyStatus === ApplyStatus.Idle) { + onApply({ + type: 'match_search_files', + filepath: path, + query: query, + file_pattern: ".md", + }) + } + }, [finish]) + + return ( +
+
+
+ + {t('chat.reactMarkdown.matchSearchInPath').replace('{query}', query).replace('{path}', path)} +
+
+
+ ) +} diff --git a/src/components/chat-view/ReactMarkdown.tsx b/src/components/chat-view/ReactMarkdown.tsx index 2db19d7..1797fad 100644 --- a/src/components/chat-view/ReactMarkdown.tsx +++ b/src/components/chat-view/ReactMarkdown.tsx @@ -13,6 +13,7 @@ import MarkdownFetchUrlsContentBlock from './Markdown/MarkdownFetchUrlsContentBl import MarkdownListFilesBlock from './Markdown/MarkdownListFilesBlock' import MarkdownReadFileBlock from './Markdown/MarkdownReadFileBlock' import MarkdownReasoningBlock from './Markdown/MarkdownReasoningBlock' +import MarkdownMatchSearchFilesBlock from './Markdown/MarkdownMatchSearchFilesBlock' import MarkdownRegexSearchFilesBlock from './Markdown/MarkdownRegexSearchFilesBlock' import MarkdownSearchAndReplace from './Markdown/MarkdownSearchAndReplace' import MarkdownSearchWebBlock from './Markdown/MarkdownSearchWebBlock' @@ -117,6 +118,15 @@ function ReactMarkdown({ recursive={block.recursive} finish={block.finish} /> + ) : block.type === 'match_search_files' ? ( + ) : block.type === 'regex_search_files' ? ( +Directory path here +Your keyword/phrase here + + +Example: Requesting to search for all Markdown files containing 'test' in the current directory + +. +test +` +} + export function getRegexSearchFilesDescription(args: ToolArgs): string { return `## regex_search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. diff --git a/src/core/search/match/coreplugin-match.ts b/src/core/search/match/coreplugin-match.ts new file mode 100644 index 0000000..f57b0eb --- /dev/null +++ b/src/core/search/match/coreplugin-match.ts @@ -0,0 +1,85 @@ +import { App } from "obsidian"; +import { + MAX_RESULTS, + truncateLine, + SearchResult, + formatResults, +} from '../search-common'; + +/** + * Searches using Obsidian's core search plugin and builds context for each match. + * + * @param app The Obsidian App instance. + * @param query The query to search for. + * @returns A promise that resolves to a formatted string of search results. + */ +export async function searchFilesWithCorePlugin( + query: string, + app: App, +): Promise { + const searchPlugin = (app as any).internalPlugins.plugins['global-search']?.instance; + if (!searchPlugin) { + throw new Error("Core search plugin is not available."); + } + + // The core search function is not officially documented and may change. + // This is based on community findings and common usage in other plugins. + const searchResults = await new Promise((resolve) => { + const unregister = searchPlugin.on("search-results", (results: any) => { + unregister(); + resolve(results); + }); + searchPlugin.openGlobalSearch(query); + }); + + const results: SearchResult[] = []; + const vault = app.vault; + + for (const fileMatches of Object.values(searchResults) as any) { + if (results.length >= MAX_RESULTS) { + break; + } + + const file = vault.getAbstractFileByPath(fileMatches.file.path); + if (!file || !('read' in file)) { + continue; + } + + const content = await vault.cachedRead(file as any); + const lines = content.split('\n'); + + for (const match of fileMatches.result.content) { + if (results.length >= MAX_RESULTS) { + break; + } + + const [matchText, startOffset] = match; + let charCount = 0; + let lineNumber = 0; + let column = 0; + let lineContent = ""; + + for (let i = 0; i < lines.length; i++) { + const lineLength = lines[i].length + 1; // +1 for the newline character + if (charCount + lineLength > startOffset) { + lineNumber = i + 1; + column = startOffset - charCount + 1; + lineContent = lines[i]; + break; + } + charCount += lineLength; + } + + results.push({ + file: fileMatches.file.path, + line: lineNumber, + column: column, + match: truncateLine(lineContent.trimEnd()), + beforeContext: lineNumber > 1 ? [truncateLine(lines[lineNumber - 2].trimEnd())] : [], + afterContext: lineNumber < lines.length ? [truncateLine(lines[lineNumber].trimEnd())] : [], + }); + } + } + + return formatResults(results, ".\\"); +} \ No newline at end of file diff --git a/src/core/regex/omnisearch-index.ts b/src/core/search/match/omnisearch-match.ts similarity index 87% rename from src/core/regex/omnisearch-index.ts rename to src/core/search/match/omnisearch-match.ts index 15322c5..5ab16f4 100644 --- a/src/core/regex/omnisearch-index.ts +++ b/src/core/search/match/omnisearch-match.ts @@ -4,9 +4,7 @@ import { truncateLine, SearchResult, formatResults, -} from './regex-common'; - -// --- Omnisearch API and Helper Types --- +} from '../search-common'; type SearchMatchApi = { match: string; @@ -69,14 +67,12 @@ function findLineAndColumnFromOffset( } /** - * Searches using Omnisearch and builds context for each match to replicate ripgrep's output. - * @param vaultPath The absolute path of the vault for making relative paths. + * Searches using Omnisearch and builds context for each match. * @param query The search query for Omnisearch. Note: Omnisearch does not support full regex. * @param app The Obsidian App instance. * @returns A formatted string of search results. */ -export async function regexSearchFilesWithOmnisearch( - vaultPath: string, +export async function searchFilesWithOmnisearch( query: string, app: App, ): Promise { @@ -87,8 +83,8 @@ export async function regexSearchFilesWithOmnisearch( ); } - // Omnisearch is not a regex engine. The function name is kept for consistency - // but the `query` will be treated as a keyword/fuzzy search by the plugin. + // Omnisearch is not a regex engine. + // The `query` will be treated as a keyword/fuzzy search by the plugin. const apiResults = await window.omnisearch.search(query); if (!apiResults || apiResults.length === 0) { throw new Error("No results found."); @@ -132,7 +128,7 @@ export async function regexSearchFilesWithOmnisearch( } } - return formatResults(results, vaultPath); + return formatResults(results, ".\\"); } catch (error) { console.error("Error during Omnisearch processing:", error); return "An error occurred during the search."; diff --git a/src/core/search/regex/coreplugin-regex.ts b/src/core/search/regex/coreplugin-regex.ts new file mode 100644 index 0000000..d29ad54 --- /dev/null +++ b/src/core/search/regex/coreplugin-regex.ts @@ -0,0 +1,17 @@ +import { App } from "obsidian"; +import { searchFilesWithCorePlugin } from '../match/coreplugin-match' + +/** + * Performs a regular expression search using Obsidian's core search plugin. + * + * @param app The Obsidian App instance. + * @param regex The regular expression to search for. + * @returns A promise that resolves to a formatted string of search results. + */ +export async function regexSearchFilesWithCorePlugin( + regex: string, + app: App, +): Promise { + const query = "/" + regex + "/"; + return searchFilesWithCorePlugin(query, app); +} \ No newline at end of file diff --git a/src/core/regex/ripgrep-index.ts b/src/core/search/regex/ripgrep-regex.ts similarity index 99% rename from src/core/regex/ripgrep-index.ts rename to src/core/search/regex/ripgrep-regex.ts index eb972dc..fa3bf39 100644 --- a/src/core/regex/ripgrep-index.ts +++ b/src/core/search/regex/ripgrep-regex.ts @@ -8,7 +8,7 @@ import { truncateLine, SearchResult, formatResults -} from './regex-common' +} from '../search-common' const isWindows = /^win/.test(process.platform) const binName = isWindows ? "rg.exe" : "rg" diff --git a/src/core/regex/regex-common.ts b/src/core/search/search-common.ts similarity index 100% rename from src/core/regex/regex-common.ts rename to src/core/search/search-common.ts diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 868ecc0..3f2b8c5 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -81,6 +81,7 @@ export default { copy: "Copy", editOrApplyDiff: "{mode}: {path}", loading: "Loading...", + matchSearchInPath: 'match search files "{query}" in {path}', regexSearchInPath: 'regex search files "{regex}" in {path}', createNewNote: "Create new note", copyMsg: "Copy message", @@ -227,10 +228,13 @@ export default { auto: 'Auto', semantic: 'Semantic', regex: 'Regex', + match: 'Match', regexBackend: 'Regex search backend', regexBackendDescription: 'Choose the backend for regex search method.', + matchBackend: 'Match search backend', + matchBackendDescription: 'Choose the backend for match search method.', ripgrep: 'ripgrep', - omnisearch: 'Omnisearch', + coreplugin: 'Core plugin', ripgrepPath: 'ripgrep path', ripgrepPathDescription: 'Path to the ripgrep binary. When using ripgrep regex search, this is required.', }, diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index 8fe4c84..05c813d 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -82,6 +82,7 @@ export default { copy: "复制", editOrApplyDiff: "{mode}:{path}", loading: "加载中...", + matchSearchInPath: '在 {path} 中匹配搜索文件 "{query}"', regexSearchInPath: '在 {path} 中正则搜索文件 "{regex}"', createNewNote: "创建新笔记", copyMsg: "复制消息", @@ -228,10 +229,13 @@ export default { auto: '自动', semantic: '语义', regex: '正则', + match: '匹配', regexBackend: '正则搜索后端', regexBackendDescription: '选择正则搜索的后端。', + matchBackend: '匹配搜索后端', + matchBackendDescription: '选择匹配搜索的后端。', ripgrep: 'ripgrep', - omnisearch: 'Omnisearch', + coreplugin: '核心插件', ripgrepPath: 'ripgrep 路径', ripgrepPathDescription: 'ripgrep 二进制文件的路径。使用 ripgrep 正则搜索时需要此项。', }, diff --git a/src/settings/SettingTab.tsx b/src/settings/SettingTab.tsx index fba345e..ac0fde0 100644 --- a/src/settings/SettingTab.tsx +++ b/src/settings/SettingTab.tsx @@ -155,26 +155,42 @@ export class InfioSettingTab extends PluginSettingTab { .addOption('auto', t('settings.FilesSearch.auto')) .addOption('semantic', t('settings.FilesSearch.semantic')) .addOption('regex', t('settings.FilesSearch.regex')) + .addOption('match', t('settings.FilesSearch.match')) .setValue(this.plugin.settings.filesSearchMethod) .onChange(async (value) => { await this.plugin.setSettings({ ...this.plugin.settings, - filesSearchMethod: value as 'regex' | 'semantic' | 'auto', + filesSearchMethod: value as 'match' | 'regex' | 'semantic' | 'auto', }) }), ) - new Setting(containerEl) + new Setting(containerEl) .setName(t('settings.FilesSearch.regexBackend')) .setDesc(t('settings.FilesSearch.regexBackendDescription')) .addDropdown((dropdown) => dropdown .addOption('ripgrep', t('settings.FilesSearch.ripgrep')) - .addOption('omnisearch', t('settings.FilesSearch.omnisearch')) + .addOption('coreplugin', t('settings.FilesSearch.coreplugin')) .setValue(this.plugin.settings.regexSearchBackend) .onChange(async (value) => { await this.plugin.setSettings({ ...this.plugin.settings, - regexSearchBackend: value as 'ripgrep' | 'omnisearch', + regexSearchBackend: value as 'ripgrep' | 'coreplugin', + }) + }), + ) + new Setting(containerEl) + .setName(t('settings.FilesSearch.matchBackend')) + .setDesc(t('settings.FilesSearch.matchBackendDescription')) + .addDropdown((dropdown) => + dropdown + .addOption('coreplugin', t('settings.FilesSearch.coreplugin')) + .addOption('omnisearch', t('settings.FilesSearch.omnisearch')) + .setValue(this.plugin.settings.matchSearchBackend) + .onChange(async (value) => { + await this.plugin.setSettings({ + ...this.plugin.settings, + matchSearchBackend: value as 'coreplugin' | 'omnisearch', }) }), ) diff --git a/src/types/apply.ts b/src/types/apply.ts index 60fca7b..184d83a 100644 --- a/src/types/apply.ts +++ b/src/types/apply.ts @@ -20,6 +20,14 @@ export type ListFilesToolArgs = { recursive?: boolean; } +export type MatchSearchFilesToolArgs = { + type: 'match_search_files'; + filepath?: string; + query?: string; + file_pattern?: string; + finish?: boolean; +} + export type RegexSearchFilesToolArgs = { type: 'regex_search_files'; filepath?: string; @@ -97,4 +105,4 @@ export type UseMcpToolArgs = { parameters: Record; } -export type ToolArgs = ReadFileToolArgs | WriteToFileToolArgs | InsertContentToolArgs | SearchAndReplaceToolArgs | ListFilesToolArgs | RegexSearchFilesToolArgs | SemanticSearchFilesToolArgs | SearchWebToolArgs | FetchUrlsContentToolArgs | SwitchModeToolArgs | ApplyDiffToolArgs | UseMcpToolArgs; +export type ToolArgs = ReadFileToolArgs | WriteToFileToolArgs | InsertContentToolArgs | SearchAndReplaceToolArgs | ListFilesToolArgs | MatchSearchFilesToolArgs | RegexSearchFilesToolArgs | SemanticSearchFilesToolArgs | SearchWebToolArgs | FetchUrlsContentToolArgs | SwitchModeToolArgs | ApplyDiffToolArgs | UseMcpToolArgs; diff --git a/src/types/settings.ts b/src/types/settings.ts index a67ae9f..582e655 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -260,8 +260,9 @@ export const InfioSettingsSchema = z.object({ jinaApiKey: z.string().catch(''), // Files Search - filesSearchMethod: z.enum(['regex', 'semantic', 'auto']).catch('auto'), - regexSearchBackend: z.enum(['omnisearch', 'ripgrep']).catch('ripgrep'), + filesSearchMethod: z.enum(['match', 'regex', 'semantic', 'auto']).catch('auto'), + regexSearchBackend: z.enum(['coreplugin', 'ripgrep']).catch('ripgrep'), + matchSearchBackend: z.enum(['omnisearch', 'coreplugin']).catch('coreplugin'), ripgrepPath: z.string().catch(''), /// [compatible] diff --git a/src/utils/glob-utils.ts b/src/utils/glob-utils.ts index 044b088..77c6489 100644 --- a/src/utils/glob-utils.ts +++ b/src/utils/glob-utils.ts @@ -28,6 +28,10 @@ export const listFilesAndFolders = async (vault: Vault, path: string) => { return [] } +export const matchSearchFiles = async (vault: Vault, path: string, query: string, file_pattern: string) => { + +} + export const regexSearchFiles = async (vault: Vault, path: string, regex: string, file_pattern: string) => { } diff --git a/src/utils/parse-infio-block.ts b/src/utils/parse-infio-block.ts index 3c5f149..7d79008 100644 --- a/src/utils/parse-infio-block.ts +++ b/src/utils/parse-infio-block.ts @@ -59,6 +59,11 @@ export type ParsedMsgBlock = path: string recursive?: boolean finish: boolean + } | { + type: 'match_search_files' + path: string + query: string + finish: boolean } | { type: 'regex_search_files' path: string @@ -226,6 +231,36 @@ export function parseMsgBlocks( finish: node.sourceCodeLocation.endTag !== undefined }) lastEndOffset = endOffset + } else if (node.nodeName === 'match_search_files') { + if (!node.sourceCodeLocation) { + throw new Error('sourceCodeLocation is undefined') + } + const startOffset = node.sourceCodeLocation.startOffset + const endOffset = node.sourceCodeLocation.endOffset + if (startOffset > lastEndOffset) { + parsedResult.push({ + type: 'string', + content: input.slice(lastEndOffset, startOffset), + }) + } + let path: string | undefined + let query: string | undefined + + for (const childNode of node.childNodes) { + if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) { + path = childNode.childNodes[0].value + } else if (childNode.nodeName === 'query' && childNode.childNodes.length > 0) { + query = childNode.childNodes[0].value + } + } + + parsedResult.push({ + type: 'match_search_files', + path: path, + query: query, + finish: node.sourceCodeLocation.endTag !== undefined + }) + lastEndOffset = endOffset } else if (node.nodeName === 'regex_search_files') { if (!node.sourceCodeLocation) { throw new Error('sourceCodeLocation is undefined') From c2dfb48e226d1e5cf580f9121b1864694fb82449 Mon Sep 17 00:00:00 2001 From: travertexg Date: Mon, 9 Jun 2025 16:46:38 +0000 Subject: [PATCH 3/8] fix: Improve search result processing and error handling - Refactor core plugin and Omnisearch result processing to use a shared helper function findLineDetails. - Update error handling in core plugin and Omnisearch search functions to return a "No results found" string instead of throwing an error when no results are found. --- src/core/search/match/coreplugin-match.ts | 99 ++++++++++++----------- src/core/search/match/omnisearch-match.ts | 42 +++------- src/core/search/regex/coreplugin-regex.ts | 4 +- src/core/search/regex/ripgrep-regex.ts | 2 +- src/core/search/search-common.ts | 26 ++++++ src/lang/locale/en.ts | 1 + src/lang/locale/zh-cn.ts | 1 + 7 files changed, 90 insertions(+), 85 deletions(-) diff --git a/src/core/search/match/coreplugin-match.ts b/src/core/search/match/coreplugin-match.ts index f57b0eb..b5aaf29 100644 --- a/src/core/search/match/coreplugin-match.ts +++ b/src/core/search/match/coreplugin-match.ts @@ -1,7 +1,8 @@ -import { App } from "obsidian"; +import { App, TFile } from "obsidian"; import { MAX_RESULTS, truncateLine, + findLineDetails, SearchResult, formatResults, } from '../search-common'; @@ -17,69 +18,69 @@ export async function searchFilesWithCorePlugin( query: string, app: App, ): Promise { - const searchPlugin = (app as any).internalPlugins.plugins['global-search']?.instance; - if (!searchPlugin) { - throw new Error("Core search plugin is not available."); - } + try { + const searchPlugin = (app as any).internalPlugins.plugins['global-search']?.instance; + if (!searchPlugin) { + throw new Error("Core search plugin is not available."); + } - // The core search function is not officially documented and may change. - // This is based on community findings and common usage in other plugins. - const searchResults = await new Promise((resolve) => { - const unregister = searchPlugin.on("search-results", (results: any) => { - unregister(); - resolve(results); - }); + // This function opens the search pane and executes the search. + // It does not return the results directly. searchPlugin.openGlobalSearch(query); - }); - const results: SearchResult[] = []; - const vault = app.vault; + // We must wait for the search to execute and the UI to update. + await new Promise(resolve => setTimeout(resolve, 500)); - for (const fileMatches of Object.values(searchResults) as any) { - if (results.length >= MAX_RESULTS) { - break; + const searchLeaf = app.workspace.getLeavesOfType('search')[0]; + if (!searchLeaf) { + throw new Error("No active search pane found after triggering search."); } - const file = vault.getAbstractFileByPath(fileMatches.file.path); - if (!file || !('read' in file)) { - continue; + // @ts-ignore + const searchResultsMap = (searchLeaf.view as any).dom.resultDomLookup; + if (!searchResultsMap || searchResultsMap.size === 0) { + console.error("No results found."); + return "No results found." } - const content = await vault.cachedRead(file as any); - const lines = content.split('\n'); + const results: SearchResult[] = []; + const vault = app.vault; - for (const match of fileMatches.result.content) { + for (const [file, fileMatches] of searchResultsMap.entries()) { if (results.length >= MAX_RESULTS) { break; } - const [matchText, startOffset] = match; - let charCount = 0; - let lineNumber = 0; - let column = 0; - let lineContent = ""; + const content = await vault.cachedRead(file as TFile); + const lines = content.split('\n'); - for (let i = 0; i < lines.length; i++) { - const lineLength = lines[i].length + 1; // +1 for the newline character - if (charCount + lineLength > startOffset) { - lineNumber = i + 1; - column = startOffset - charCount + 1; - lineContent = lines[i]; - break; - } - charCount += lineLength; + // `fileMatches.result.content` holds an array of matches for the file. + // Each match is an array: [matched_text, start_offset] + for (const match of fileMatches.result.content) { + if (results.length >= MAX_RESULTS) break; + + const startOffset = match[1]; + const { lineNumber, columnNumber, lineContent } = findLineDetails(lines, startOffset); + + if (lineNumber === -1) continue; + + results.push({ + file: file.path, + line: lineNumber + 1, // ripgrep is 1-based, so we adjust + column: columnNumber + 1, + match: truncateLine(lineContent.trimEnd()), + beforeContext: lineNumber > 0 ? [truncateLine(lines[lineNumber - 1].trimEnd())] : [], + afterContext: + lineNumber < lines.length - 1 + ? [truncateLine(lines[lineNumber + 1].trimEnd())] + : [], + }); } - - results.push({ - file: fileMatches.file.path, - line: lineNumber, - column: column, - match: truncateLine(lineContent.trimEnd()), - beforeContext: lineNumber > 1 ? [truncateLine(lines[lineNumber - 2].trimEnd())] : [], - afterContext: lineNumber < lines.length ? [truncateLine(lines[lineNumber].trimEnd())] : [], - }); } - } - return formatResults(results, ".\\"); + return formatResults(results, ".\\"); + } catch (error) { + console.error("Error during core plugin processing:", error); + return "An error occurred during the search."; + } } \ No newline at end of file diff --git a/src/core/search/match/omnisearch-match.ts b/src/core/search/match/omnisearch-match.ts index 5ab16f4..03f39b7 100644 --- a/src/core/search/match/omnisearch-match.ts +++ b/src/core/search/match/omnisearch-match.ts @@ -2,6 +2,7 @@ import { App } from "obsidian"; import { MAX_RESULTS, truncateLine, + findLineDetails, SearchResult, formatResults, } from '../search-common'; @@ -40,32 +41,6 @@ function isOmnisearchAvailable(): boolean { return window.omnisearch && typeof window.omnisearch.search === "function"; } -/** - * Finds the line number, column number, and content for a given character offset in a file. - * @param allLines All lines in the file. - * @param offset The character offset of the match. - * @returns An object with line number, column number, and the full line content. - */ -function findLineAndColumnFromOffset( - allLines: string[], - offset: number -): { lineNumber: number; columnNumber: number; lineContent: string } { - let charCount = 0; - for (let i = 0; i < allLines.length; i++) { - const line = allLines[i]; - // The line ending length (1 for \n, 2 for \r\n) can vary. - // A simple +1 is a reasonable approximation for this calculation. - const lineEndOffset = charCount + line.length + 1; - - if (offset < lineEndOffset) { - const columnNumber = offset - charCount; - return { lineNumber: i, columnNumber, lineContent: line }; - } - charCount = lineEndOffset; - } - return { lineNumber: -1, columnNumber: -1, lineContent: "" }; -} - /** * Searches using Omnisearch and builds context for each match. * @param query The search query for Omnisearch. Note: Omnisearch does not support full regex. @@ -87,7 +62,8 @@ export async function searchFilesWithOmnisearch( // The `query` will be treated as a keyword/fuzzy search by the plugin. const apiResults = await window.omnisearch.search(query); if (!apiResults || apiResults.length === 0) { - throw new Error("No results found."); + console.error("No results found."); + return "No results found." } const results: SearchResult[] = []; @@ -99,15 +75,15 @@ export async function searchFilesWithOmnisearch( if (!result.matches || result.matches.length === 0) continue; const fileContent = await app.vault.adapter.read(result.path); - const allLines = fileContent.split("\n"); + const lines = fileContent.split("\n"); for (const match of result.matches) { if (results.length >= MAX_RESULTS) { break; // Stop processing matches if we have enough results } - const { lineNumber, columnNumber, lineContent } = findLineAndColumnFromOffset( - allLines, + const { lineNumber, columnNumber, lineContent } = findLineDetails( + lines, match.offset ); @@ -118,10 +94,10 @@ export async function searchFilesWithOmnisearch( line: lineNumber + 1, // ripgrep is 1-based, so we adjust column: columnNumber + 1, match: truncateLine(lineContent.trimEnd()), - beforeContext: lineNumber > 0 ? [truncateLine(allLines[lineNumber - 1].trimEnd())] : [], + beforeContext: lineNumber > 0 ? [truncateLine(lines[lineNumber - 1].trimEnd())] : [], afterContext: - lineNumber < allLines.length - 1 - ? [truncateLine(allLines[lineNumber + 1].trimEnd())] + lineNumber < lines.length - 1 + ? [truncateLine(lines[lineNumber + 1].trimEnd())] : [], }; results.push(searchResult); diff --git a/src/core/search/regex/coreplugin-regex.ts b/src/core/search/regex/coreplugin-regex.ts index d29ad54..4e233f8 100644 --- a/src/core/search/regex/coreplugin-regex.ts +++ b/src/core/search/regex/coreplugin-regex.ts @@ -12,6 +12,6 @@ export async function regexSearchFilesWithCorePlugin( regex: string, app: App, ): Promise { - const query = "/" + regex + "/"; - return searchFilesWithCorePlugin(query, app); + const regexQuery = `/${regex}/`; + return searchFilesWithCorePlugin(regexQuery, app); } \ No newline at end of file diff --git a/src/core/search/regex/ripgrep-regex.ts b/src/core/search/regex/ripgrep-regex.ts index fa3bf39..3adf6c7 100644 --- a/src/core/search/regex/ripgrep-regex.ts +++ b/src/core/search/regex/ripgrep-regex.ts @@ -96,7 +96,7 @@ export async function regexSearchFilesWithRipgrep( output = await execRipgrep(rgPath, args) } catch (error) { console.error("Error executing ripgrep:", error) - return "No results found" + return "No results found." } const results: SearchResult[] = [] let currentResult: Partial | null = null diff --git a/src/core/search/search-common.ts b/src/core/search/search-common.ts index 0e5b3e7..e16c1b8 100644 --- a/src/core/search/search-common.ts +++ b/src/core/search/search-common.ts @@ -14,6 +14,32 @@ export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line } +/** + * Finds the line number and content for a given character offset within a file's content. + * @param lines All lines in the file. + * @param offset The character offset of the match. + * @returns An object with line number, column number, and the full line content. + */ +export function findLineDetails( + lines: string[], + offset: number +): { lineNumber: number; columnNumber: number; lineContent: string } { + let charCount = 0; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + // The line ending length (1 for \n, 2 for \r\n) can vary. + // A simple +1 is a reasonable approximation for this calculation. + const lineEndOffset = charCount + line.length + 1; + + if (offset < lineEndOffset) { + const columnNumber = offset - charCount; + return { lineNumber: i, columnNumber, lineContent: line }; + } + charCount = lineEndOffset; + } + return { lineNumber: -1, columnNumber: -1, lineContent: "" }; +} + export interface SearchResult { file: string line: number diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 3f2b8c5..0374b61 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -235,6 +235,7 @@ export default { matchBackendDescription: 'Choose the backend for match search method.', ripgrep: 'ripgrep', coreplugin: 'Core plugin', + omnisearch: 'Omnisearch', ripgrepPath: 'ripgrep path', ripgrepPathDescription: 'Path to the ripgrep binary. When using ripgrep regex search, this is required.', }, diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index 05c813d..99081ca 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -236,6 +236,7 @@ export default { matchBackendDescription: '选择匹配搜索的后端。', ripgrep: 'ripgrep', coreplugin: '核心插件', + omnisearch: 'Omnisearch', ripgrepPath: 'ripgrep 路径', ripgrepPathDescription: 'ripgrep 二进制文件的路径。使用 ripgrep 正则搜索时需要此项。', }, From 250a0e1bde46f0ab0df80a5ab5e4b648d5969e76 Mon Sep 17 00:00:00 2001 From: travertexg Date: Mon, 9 Jun 2025 16:52:36 +0000 Subject: [PATCH 4/8] Refactor: Rename file search functions for clarity --- src/components/chat-view/ChatView.tsx | 16 ++++++++-------- src/core/search/match/coreplugin-match.ts | 2 +- src/core/search/match/omnisearch-match.ts | 2 +- src/core/search/regex/coreplugin-regex.ts | 6 +++--- src/core/search/regex/ripgrep-regex.ts | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/chat-view/ChatView.tsx b/src/components/chat-view/ChatView.tsx index a477843..d236812 100644 --- a/src/components/chat-view/ChatView.tsx +++ b/src/components/chat-view/ChatView.tsx @@ -29,10 +29,10 @@ import { LLMBaseUrlNotSetException, LLMModelNotSetException, } from '../../core/llm/exception' -import { searchFilesWithCorePlugin } from '../../core/search/match/coreplugin-match' -import { searchFilesWithOmnisearch } from '../../core/search/match/omnisearch-match' -import { regexSearchFilesWithRipgrep } from '../../core/search/regex/ripgrep-regex' -import { regexSearchFilesWithCorePlugin } from '../../core/search/regex/coreplugin-regex' +import { matchSearchUsingCorePlugin } from '../../core/search/match/coreplugin-match' +import { matchSearchUsingOmnisearch } from '../../core/search/match/omnisearch-match' +import { regexSearchUsingRipgrep } from '../../core/search/regex/ripgrep-regex' +import { regexSearchUsingCorePlugin } from '../../core/search/regex/coreplugin-regex' import { useChatHistory } from '../../hooks/use-chat-history' import { useCustomModes } from '../../hooks/use-custom-mode' import { t } from '../../lang/helpers' @@ -614,9 +614,9 @@ const Chat = forwardRef((props, ref) => { const searchBackend = settings.matchSearchBackend let results: string; if (searchBackend === 'omnisearch') { - results = await searchFilesWithOmnisearch(toolArgs.query, app) + results = await matchSearchUsingOmnisearch(toolArgs.query, app) } else { - results = await searchFilesWithCorePlugin(toolArgs.query, app) + results = await matchSearchUsingCorePlugin(toolArgs.query, app) } const formattedContent = `[match_search_files for '${toolArgs.filepath}'] Result:\n${results}\n`; return { @@ -636,13 +636,13 @@ const Chat = forwardRef((props, ref) => { const searchBackend = settings.regexSearchBackend let results: string; if (searchBackend === 'coreplugin') { - results = await regexSearchFilesWithCorePlugin(toolArgs.regex, app) + results = await regexSearchUsingCorePlugin(toolArgs.regex, app) } else { // @ts-expect-error Obsidian API type mismatch const baseVaultPath = String(app.vault.adapter.getBasePath()) const absolutePath = path.join(baseVaultPath, toolArgs.filepath) const ripgrepPath = settings.ripgrepPath - results = await regexSearchFilesWithRipgrep(absolutePath, toolArgs.regex, ripgrepPath) + results = await regexSearchUsingRipgrep(absolutePath, toolArgs.regex, ripgrepPath) } const formattedContent = `[regex_search_files for '${toolArgs.filepath}'] Result:\n${results}\n`; return { diff --git a/src/core/search/match/coreplugin-match.ts b/src/core/search/match/coreplugin-match.ts index b5aaf29..c58b5b2 100644 --- a/src/core/search/match/coreplugin-match.ts +++ b/src/core/search/match/coreplugin-match.ts @@ -14,7 +14,7 @@ import { * @param query The query to search for. * @returns A promise that resolves to a formatted string of search results. */ -export async function searchFilesWithCorePlugin( +export async function matchSearchUsingCorePlugin( query: string, app: App, ): Promise { diff --git a/src/core/search/match/omnisearch-match.ts b/src/core/search/match/omnisearch-match.ts index 03f39b7..7865600 100644 --- a/src/core/search/match/omnisearch-match.ts +++ b/src/core/search/match/omnisearch-match.ts @@ -47,7 +47,7 @@ function isOmnisearchAvailable(): boolean { * @param app The Obsidian App instance. * @returns A formatted string of search results. */ -export async function searchFilesWithOmnisearch( +export async function matchSearchUsingOmnisearch( query: string, app: App, ): Promise { diff --git a/src/core/search/regex/coreplugin-regex.ts b/src/core/search/regex/coreplugin-regex.ts index 4e233f8..3c52acc 100644 --- a/src/core/search/regex/coreplugin-regex.ts +++ b/src/core/search/regex/coreplugin-regex.ts @@ -1,5 +1,5 @@ import { App } from "obsidian"; -import { searchFilesWithCorePlugin } from '../match/coreplugin-match' +import { matchSearchUsingCorePlugin } from '../match/coreplugin-match' /** * Performs a regular expression search using Obsidian's core search plugin. @@ -8,10 +8,10 @@ import { searchFilesWithCorePlugin } from '../match/coreplugin-match' * @param regex The regular expression to search for. * @returns A promise that resolves to a formatted string of search results. */ -export async function regexSearchFilesWithCorePlugin( +export async function regexSearchUsingCorePlugin( regex: string, app: App, ): Promise { const regexQuery = `/${regex}/`; - return searchFilesWithCorePlugin(regexQuery, app); + return matchSearchUsingCorePlugin(regexQuery, app); } \ No newline at end of file diff --git a/src/core/search/regex/ripgrep-regex.ts b/src/core/search/regex/ripgrep-regex.ts index 3adf6c7..0974164 100644 --- a/src/core/search/regex/ripgrep-regex.ts +++ b/src/core/search/regex/ripgrep-regex.ts @@ -66,7 +66,7 @@ async function execRipgrep(bin: string, args: string[]): Promise { }) } -export async function regexSearchFilesWithRipgrep( +export async function regexSearchUsingRipgrep( directoryPath: string, regex: string, ripgrepPath: string, From a00b640dad1acb8e059f884ac18fcb2006010399 Mon Sep 17 00:00:00 2001 From: travertexg Date: Tue, 10 Jun 2025 04:21:40 +0000 Subject: [PATCH 5/8] refactor: Improve file search tool descriptions and reliability - Updates the description for the regex_search_files tool to dynamically indicate the regex syntax used based on the user's settings (ECMAScript or Rust). - Refactors the core plugin match search to wait for the search view to load before attempting to access results, addressing a timing issue. - Adds new settings for regexSearchBackend and matchSearchBackend in settings.test.ts to reflect the refactored backend options for file searching. --- src/core/prompts/tools/search-files.ts | 18 ++++++++++++++++-- src/core/search/match/coreplugin-match.ts | 17 +++++++++++------ src/types/settings.test.ts | 4 ++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/core/prompts/tools/search-files.ts b/src/core/prompts/tools/search-files.ts index 8ed702b..3008b6f 100644 --- a/src/core/prompts/tools/search-files.ts +++ b/src/core/prompts/tools/search-files.ts @@ -1,4 +1,5 @@ import { ToolArgs } from "./types" +import { useSettings } from '../../../contexts/SettingsContext' export function getSearchFilesDescription(args: ToolArgs): string { if (args.searchTool === 'match') { @@ -17,7 +18,7 @@ export function getMatchSearchFilesDescription(args: ToolArgs): string { Description: Request to perform a match/fuzzy search across files in a specified directory, providing context-rich results. This tool searches for specific content across multiple files, displaying each match with encapsulating context. Parameters: - path: (required) The path of the directory to search in (relative to the current working directory ${args.cwd}). This directory will be recursively searched. -- query: (required) The keyword, phrase to search for. The system will find documents with similar keywords/phrases. +- query: (required) The keyword/phrase to search for. The system will find documents with similar keywords/phrases. Usage: @@ -33,11 +34,24 @@ Example: Requesting to search for all Markdown files containing 'test' in the cu } export function getRegexSearchFilesDescription(args: ToolArgs): string { + const { settings } = useSettings() + let regex_syntax: string; + switch (settings.regexSearchBackend) { + case 'coreplugin': + regex_syntax = "ECMAScript (JavaScript)"; + break; + case 'ripgrep': + regex_syntax = "Rust"; + break; + default: + regex_syntax = "ECMAScript (JavaScript)"; + } + return `## regex_search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: - path: (required) The path of the directory to search in (relative to the current working directory ${args.cwd}). This directory will be recursively searched. -- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax, **but should not include word boundaries (\b)**. +- regex: (required) The regular expression pattern to search for. Uses ${regex_syntax} regex syntax, **but should not include word boundaries (\b)**. Usage: diff --git a/src/core/search/match/coreplugin-match.ts b/src/core/search/match/coreplugin-match.ts index c58b5b2..d32e955 100644 --- a/src/core/search/match/coreplugin-match.ts +++ b/src/core/search/match/coreplugin-match.ts @@ -28,18 +28,23 @@ export async function matchSearchUsingCorePlugin( // It does not return the results directly. searchPlugin.openGlobalSearch(query); - // We must wait for the search to execute and the UI to update. - await new Promise(resolve => setTimeout(resolve, 500)); - const searchLeaf = app.workspace.getLeavesOfType('search')[0]; if (!searchLeaf) { throw new Error("No active search pane found after triggering search."); } - // @ts-ignore - const searchResultsMap = (searchLeaf.view as any).dom.resultDomLookup; + // Ensure the view is fully loaded before we try to access its properties. + const view = await searchLeaf.open(searchLeaf.view); + const searchResultsMap = await new Promise>(resolve => { + setTimeout(() => { + // @ts-ignore + const results = (view as any).dom?.resultDomLookup; + resolve(results || new Map()); + }, 5000) + }); + if (!searchResultsMap || searchResultsMap.size === 0) { - console.error("No results found."); + console.error("No results found or search results map is not available."); return "No results found." } diff --git a/src/types/settings.test.ts b/src/types/settings.test.ts index cec6c6c..3d73299 100644 --- a/src/types/settings.test.ts +++ b/src/types/settings.test.ts @@ -13,6 +13,8 @@ describe('parseSmartCopilotSettings', () => { openAIApiKey: '', anthropicApiKey: '', filesSearchMethod: 'auto', + regexSearchBackend: 'ripgrep', + matchSearchBackend: 'coreplugin', fuzzyMatchThreshold: 0.85, geminiApiKey: '', groqApiKey: '', @@ -196,6 +198,8 @@ describe('settings migration', () => { openAIApiKey: '', anthropicApiKey: '', filesSearchMethod: 'auto', + regexSearchBackend: 'ripgrep', + matchSearchBackend: 'coreplugin', fuzzyMatchThreshold: 0.85, geminiApiKey: '', groqApiKey: '', From f0be561cfc59525f2cbba1968f1f418c9c58b84f Mon Sep 17 00:00:00 2001 From: travertexg Date: Tue, 10 Jun 2025 06:54:25 +0000 Subject: [PATCH 6/8] refactor: Restructure file search settings This commit restructures the file search settings. The previously individual settings for file search method, regex search backend, match search backend, and ripgrep path have been grouped into a new filesSearchSettings object. --- src/components/chat-view/ChatView.tsx | 6 ++--- src/components/chat-view/CustomModeView.tsx | 2 +- src/core/prompts/system.ts | 5 ++++ src/core/prompts/tools/index.ts | 3 +++ src/core/prompts/tools/search-files.ts | 4 +-- src/core/prompts/tools/types.ts | 2 ++ src/settings/SettingTab.tsx | 28 +++++++++++++++------ src/types/settings.test.ts | 20 +++++++++------ src/types/settings.ts | 18 ++++++++++--- src/utils/prompt-generator.ts | 3 ++- 10 files changed, 63 insertions(+), 28 deletions(-) diff --git a/src/components/chat-view/ChatView.tsx b/src/components/chat-view/ChatView.tsx index d236812..e6422d2 100644 --- a/src/components/chat-view/ChatView.tsx +++ b/src/components/chat-view/ChatView.tsx @@ -611,7 +611,7 @@ const Chat = forwardRef((props, ref) => { } } } else if (toolArgs.type === 'match_search_files') { - const searchBackend = settings.matchSearchBackend + const searchBackend = settings.filesSearchSettings.matchBackend let results: string; if (searchBackend === 'omnisearch') { results = await matchSearchUsingOmnisearch(toolArgs.query, app) @@ -633,7 +633,7 @@ const Chat = forwardRef((props, ref) => { } } } else if (toolArgs.type === 'regex_search_files') { - const searchBackend = settings.regexSearchBackend + const searchBackend = settings.filesSearchSettings.regexBackend let results: string; if (searchBackend === 'coreplugin') { results = await regexSearchUsingCorePlugin(toolArgs.regex, app) @@ -641,7 +641,7 @@ const Chat = forwardRef((props, ref) => { // @ts-expect-error Obsidian API type mismatch const baseVaultPath = String(app.vault.adapter.getBasePath()) const absolutePath = path.join(baseVaultPath, toolArgs.filepath) - const ripgrepPath = settings.ripgrepPath + const ripgrepPath = settings.filesSearchSettings.ripgrepPath results = await regexSearchUsingRipgrep(absolutePath, toolArgs.regex, ripgrepPath) } const formattedContent = `[regex_search_files for '${toolArgs.filepath}'] Result:\n${results}\n`; diff --git a/src/components/chat-view/CustomModeView.tsx b/src/components/chat-view/CustomModeView.tsx index ce89927..20135c6 100644 --- a/src/components/chat-view/CustomModeView.tsx +++ b/src/components/chat-view/CustomModeView.tsx @@ -367,7 +367,7 @@ const CustomModeView = () => { {t('prompt.overrideWarning')}