Merge pull request #82 from travertexs/file-search
feat: Added Core Search Plugin and Omnisearch Support for Local File Searching
This commit is contained in:
commit
8f600098fa
@ -29,7 +29,10 @@ import {
|
|||||||
LLMBaseUrlNotSetException,
|
LLMBaseUrlNotSetException,
|
||||||
LLMModelNotSetException,
|
LLMModelNotSetException,
|
||||||
} from '../../core/llm/exception'
|
} from '../../core/llm/exception'
|
||||||
import { regexSearchFiles } from '../../core/ripgrep'
|
import { matchSearchUsingCorePlugin } from '../../core/file-search/match/coreplugin-match'
|
||||||
|
import { matchSearchUsingOmnisearch } from '../../core/file-search/match/omnisearch-match'
|
||||||
|
import { regexSearchUsingRipgrep } from '../../core/file-search/regex/ripgrep-regex'
|
||||||
|
import { regexSearchUsingCorePlugin } from '../../core/file-search/regex/coreplugin-regex'
|
||||||
import { useChatHistory } from '../../hooks/use-chat-history'
|
import { useChatHistory } from '../../hooks/use-chat-history'
|
||||||
import { useCustomModes } from '../../hooks/use-custom-mode'
|
import { useCustomModes } from '../../hooks/use-custom-mode'
|
||||||
import { t } from '../../lang/helpers'
|
import { t } from '../../lang/helpers'
|
||||||
@ -607,12 +610,40 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
|||||||
mentionables: [],
|
mentionables: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (toolArgs.type === 'match_search_files') {
|
||||||
|
const searchBackend = settings.filesSearchSettings.matchBackend
|
||||||
|
let results: string;
|
||||||
|
if (searchBackend === 'omnisearch') {
|
||||||
|
results = await matchSearchUsingOmnisearch(toolArgs.query, app)
|
||||||
|
} else {
|
||||||
|
results = await matchSearchUsingCorePlugin(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') {
|
} else if (toolArgs.type === 'regex_search_files') {
|
||||||
|
const searchBackend = settings.filesSearchSettings.regexBackend
|
||||||
|
let results: string;
|
||||||
|
if (searchBackend === 'coreplugin') {
|
||||||
|
results = await regexSearchUsingCorePlugin(toolArgs.regex, app)
|
||||||
|
} else {
|
||||||
// @ts-expect-error Obsidian API type mismatch
|
// @ts-expect-error Obsidian API type mismatch
|
||||||
const baseVaultPath = String(app.vault.adapter.getBasePath())
|
const baseVaultPath = String(app.vault.adapter.getBasePath())
|
||||||
const ripgrepPath = settings.ripgrepPath
|
|
||||||
const absolutePath = path.join(baseVaultPath, toolArgs.filepath)
|
const absolutePath = path.join(baseVaultPath, toolArgs.filepath)
|
||||||
const results = await regexSearchFiles(absolutePath, toolArgs.regex, 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`;
|
const formattedContent = `[regex_search_files for '${toolArgs.filepath}'] Result:\n${results}\n`;
|
||||||
return {
|
return {
|
||||||
type: 'regex_search_files',
|
type: 'regex_search_files',
|
||||||
|
|||||||
@ -367,7 +367,7 @@ const CustomModeView = () => {
|
|||||||
{t('prompt.overrideWarning')} <button
|
{t('prompt.overrideWarning')} <button
|
||||||
className="infio-preview-btn"
|
className="infio-preview-btn"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
let filesSearchMethod = settings.filesSearchMethod
|
let filesSearchMethod = settings.filesSearchSettings.method
|
||||||
if (filesSearchMethod === 'auto' && settings.embeddingModelId && settings.embeddingModelId !== '') {
|
if (filesSearchMethod === 'auto' && settings.embeddingModelId && settings.embeddingModelId !== '') {
|
||||||
filesSearchMethod = 'semantic'
|
filesSearchMethod = 'semantic'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<div
|
||||||
|
className={`infio-chat-code-block ${path ? 'has-filename' : ''}`}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<div className={'infio-chat-code-block-header'}>
|
||||||
|
<div className={'infio-chat-code-block-header-filename'}>
|
||||||
|
<FileSearch size={14} className="infio-chat-code-block-header-icon" />
|
||||||
|
<span>{t('chat.reactMarkdown.matchSearchInPath').replace('{query}', query).replace('{path}', path)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@ import MarkdownFetchUrlsContentBlock from './Markdown/MarkdownFetchUrlsContentBl
|
|||||||
import MarkdownListFilesBlock from './Markdown/MarkdownListFilesBlock'
|
import MarkdownListFilesBlock from './Markdown/MarkdownListFilesBlock'
|
||||||
import MarkdownReadFileBlock from './Markdown/MarkdownReadFileBlock'
|
import MarkdownReadFileBlock from './Markdown/MarkdownReadFileBlock'
|
||||||
import MarkdownReasoningBlock from './Markdown/MarkdownReasoningBlock'
|
import MarkdownReasoningBlock from './Markdown/MarkdownReasoningBlock'
|
||||||
|
import MarkdownMatchSearchFilesBlock from './Markdown/MarkdownMatchSearchFilesBlock'
|
||||||
import MarkdownRegexSearchFilesBlock from './Markdown/MarkdownRegexSearchFilesBlock'
|
import MarkdownRegexSearchFilesBlock from './Markdown/MarkdownRegexSearchFilesBlock'
|
||||||
import MarkdownSearchAndReplace from './Markdown/MarkdownSearchAndReplace'
|
import MarkdownSearchAndReplace from './Markdown/MarkdownSearchAndReplace'
|
||||||
import MarkdownSearchWebBlock from './Markdown/MarkdownSearchWebBlock'
|
import MarkdownSearchWebBlock from './Markdown/MarkdownSearchWebBlock'
|
||||||
@ -117,6 +118,15 @@ function ReactMarkdown({
|
|||||||
recursive={block.recursive}
|
recursive={block.recursive}
|
||||||
finish={block.finish}
|
finish={block.finish}
|
||||||
/>
|
/>
|
||||||
|
) : block.type === 'match_search_files' ? (
|
||||||
|
<MarkdownMatchSearchFilesBlock
|
||||||
|
key={"match-search-files-" + index}
|
||||||
|
applyStatus={applyStatus}
|
||||||
|
onApply={onApply}
|
||||||
|
path={block.path}
|
||||||
|
query={block.query}
|
||||||
|
finish={block.finish}
|
||||||
|
/>
|
||||||
) : block.type === 'regex_search_files' ? (
|
) : block.type === 'regex_search_files' ? (
|
||||||
<MarkdownRegexSearchFilesBlock
|
<MarkdownRegexSearchFilesBlock
|
||||||
key={"regex-search-files-" + index}
|
key={"regex-search-files-" + index}
|
||||||
|
|||||||
91
src/core/file-search/match/coreplugin-match.ts
Normal file
91
src/core/file-search/match/coreplugin-match.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { App, TFile } from "obsidian";
|
||||||
|
import {
|
||||||
|
MAX_RESULTS,
|
||||||
|
truncateLine,
|
||||||
|
findLineDetails,
|
||||||
|
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 matchSearchUsingCorePlugin(
|
||||||
|
query: string,
|
||||||
|
app: App,
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
const searchPlugin = (app as any).internalPlugins.plugins['global-search']?.instance;
|
||||||
|
if (!searchPlugin) {
|
||||||
|
throw new Error("Core search plugin is not available.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function opens the search pane and executes the search.
|
||||||
|
// It does not return the results directly.
|
||||||
|
searchPlugin.openGlobalSearch(query);
|
||||||
|
|
||||||
|
const searchLeaf = app.workspace.getLeavesOfType('search')[0];
|
||||||
|
if (!searchLeaf) {
|
||||||
|
throw new Error("No active search pane found after triggering search.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<Map<TFile, any>>(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
const results = (view as any).dom?.resultDomLookup;
|
||||||
|
resolve(results || new Map());
|
||||||
|
}, 10000)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!searchResultsMap || searchResultsMap.size === 0) {
|
||||||
|
console.error("No results found or search results map is not available.");
|
||||||
|
return "No results found."
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: SearchResult[] = [];
|
||||||
|
const vault = app.vault;
|
||||||
|
|
||||||
|
for (const [file, fileMatches] of searchResultsMap.entries()) {
|
||||||
|
if (results.length >= MAX_RESULTS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await vault.cachedRead(file as TFile);
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
// `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())]
|
||||||
|
: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatResults(results, ".\\");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during core plugin processing:", error);
|
||||||
|
return "An error occurred during the search.";
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/core/file-search/match/omnisearch-match.ts
Normal file
112
src/core/file-search/match/omnisearch-match.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { App } from "obsidian";
|
||||||
|
import {
|
||||||
|
MAX_RESULTS,
|
||||||
|
truncateLine,
|
||||||
|
findLineDetails,
|
||||||
|
SearchResult,
|
||||||
|
formatResults,
|
||||||
|
} from '../search-common';
|
||||||
|
|
||||||
|
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<ResultNoteApi[]>;
|
||||||
|
// ... 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";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 matchSearchUsingOmnisearch(
|
||||||
|
query: string,
|
||||||
|
app: App,
|
||||||
|
): Promise<string> {
|
||||||
|
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 `query` will be treated as a keyword/fuzzy search by the plugin.
|
||||||
|
const apiResults = await window.omnisearch.search(query);
|
||||||
|
if (!apiResults || apiResults.length === 0) {
|
||||||
|
console.error("No results found.");
|
||||||
|
return "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 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 } = findLineDetails(
|
||||||
|
lines,
|
||||||
|
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(lines[lineNumber - 1].trimEnd())] : [],
|
||||||
|
afterContext:
|
||||||
|
lineNumber < lines.length - 1
|
||||||
|
? [truncateLine(lines[lineNumber + 1].trimEnd())]
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
results.push(searchResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatResults(results, ".\\");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during Omnisearch processing:", error);
|
||||||
|
return "An error occurred during the search.";
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/core/file-search/regex/coreplugin-regex.ts
Normal file
17
src/core/file-search/regex/coreplugin-regex.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { App } from "obsidian";
|
||||||
|
import { matchSearchUsingCorePlugin } 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 regexSearchUsingCorePlugin(
|
||||||
|
regex: string,
|
||||||
|
app: App,
|
||||||
|
): Promise<string> {
|
||||||
|
const regexQuery = `/${regex}/`;
|
||||||
|
return matchSearchUsingCorePlugin(regexQuery, app);
|
||||||
|
}
|
||||||
@ -3,33 +3,16 @@ import * as childProcess from "child_process"
|
|||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as readline from "readline"
|
import * as readline from "readline"
|
||||||
|
import {
|
||||||
|
MAX_RESULTS,
|
||||||
|
truncateLine,
|
||||||
|
SearchResult,
|
||||||
|
formatResults
|
||||||
|
} from '../search-common'
|
||||||
|
|
||||||
const isWindows = /^win/.test(process.platform)
|
const isWindows = /^win/.test(process.platform)
|
||||||
const binName = isWindows ? "rg.exe" : "rg"
|
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<string | undefined> {
|
async function getBinPath(ripgrepPath: string): Promise<string | undefined> {
|
||||||
const binPath = path.join(ripgrepPath, binName)
|
const binPath = path.join(ripgrepPath, binName)
|
||||||
return (await pathExists(binPath)) ? binPath : undefined
|
return (await pathExists(binPath)) ? binPath : undefined
|
||||||
@ -83,7 +66,7 @@ async function execRipgrep(bin: string, args: string[]): Promise<string> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function regexSearchFiles(
|
export async function regexSearchUsingRipgrep(
|
||||||
directoryPath: string,
|
directoryPath: string,
|
||||||
regex: string,
|
regex: string,
|
||||||
ripgrepPath: string,
|
ripgrepPath: string,
|
||||||
@ -113,7 +96,7 @@ export async function regexSearchFiles(
|
|||||||
output = await execRipgrep(rgPath, args)
|
output = await execRipgrep(rgPath, args)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error executing ripgrep:", error)
|
console.error("Error executing ripgrep:", error)
|
||||||
return "No results found"
|
return "No results found."
|
||||||
}
|
}
|
||||||
const results: SearchResult[] = []
|
const results: SearchResult[] = []
|
||||||
let currentResult: Partial<SearchResult> | null = null
|
let currentResult: Partial<SearchResult> | null = null
|
||||||
@ -162,42 +145,3 @@ export async function regexSearchFiles(
|
|||||||
|
|
||||||
return formatResults(results, directoryPath)
|
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()
|
|
||||||
}
|
|
||||||
89
src/core/file-search/search-common.ts
Normal file
89
src/core/file-search/search-common.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
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()
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
const MatchSearchFilesInstructions = "\n- You can use match_search_files to perform fuzzy-based searches across files using keyword/phrase. This tool is ideal for finding similar texts in notes. It excels at finding similar contents with similar keywords and phrases quickly."
|
||||||
|
|
||||||
const RegexSearchFilesInstructions = "\n- You can use regex_search_files to perform pattern-based searches across files using regular expressions. This tool is ideal for finding exact text matches, specific patterns (like tags, links, dates, URLs), or structural elements in notes. It excels at locating precise format patterns and is perfect for finding connections between notes, frontmatter elements, or specific Markdown formatting."
|
const RegexSearchFilesInstructions = "\n- You can use regex_search_files to perform pattern-based searches across files using regular expressions. This tool is ideal for finding exact text matches, specific patterns (like tags, links, dates, URLs), or structural elements in notes. It excels at locating precise format patterns and is perfect for finding connections between notes, frontmatter elements, or specific Markdown formatting."
|
||||||
|
|
||||||
const SemanticSearchFilesInstructions = "\n- You can use semantic_search_files to find content based on meaning rather than exact text matches. Semantic search uses embedding vectors to understand concepts and ideas, finding relevant content even when keywords differ. This is especially powerful for discovering thematically related notes, answering conceptual questions about your knowledge base, or finding content when you don't know the exact wording used in the notes."
|
const SemanticSearchFilesInstructions = "\n- You can use semantic_search_files to find content based on meaning rather than exact text matches. Semantic search uses embedding vectors to understand concepts and ideas, finding relevant content even when keywords differ. This is especially powerful for discovering thematically related notes, answering conceptual questions about your knowledge base, or finding content when you don't know the exact wording used in the notes."
|
||||||
@ -6,12 +8,20 @@ function getObsidianCapabilitiesSection(
|
|||||||
cwd: string,
|
cwd: string,
|
||||||
searchFilesTool: string,
|
searchFilesTool: string,
|
||||||
): string {
|
): string {
|
||||||
|
let searchFilesInstructions: string;
|
||||||
const searchFilesInstructions = searchFilesTool === 'regex'
|
switch (searchFilesTool) {
|
||||||
? RegexSearchFilesInstructions
|
case 'match':
|
||||||
: searchFilesTool === 'semantic'
|
searchFilesInstructions = MatchSearchFilesInstructions;
|
||||||
? SemanticSearchFilesInstructions
|
break;
|
||||||
: "";
|
case 'regex':
|
||||||
|
searchFilesInstructions = RegexSearchFilesInstructions;
|
||||||
|
break;
|
||||||
|
case 'semantic':
|
||||||
|
searchFilesInstructions = SemanticSearchFilesInstructions;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
searchFilesInstructions = "";
|
||||||
|
}
|
||||||
|
|
||||||
return `====
|
return `====
|
||||||
|
|
||||||
|
|||||||
@ -57,7 +57,9 @@ function getEditingInstructions(diffStrategy?: DiffStrategy): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSearchInstructions(searchTool: string): string {
|
function getSearchInstructions(searchTool: string): string {
|
||||||
if (searchTool === 'regex') {
|
if (searchTool === 'match') {
|
||||||
|
return `- When using the match_search_files tool, craft your keyword/phrase carefully to balance specificity and flexibility. Based on the user's task, you may use it to find specific content, notes, headings, connections between notes, tags, or any text-based information across the Obsidian vault. The results include context, so analyze the surrounding text to better understand the matches. Leverage the match_search_files tool in combination with other tools for comprehensive analysis. For example, use it to find specific keywords or phrases, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.`
|
||||||
|
} else if (searchTool === 'regex') {
|
||||||
return `- When using the regex_search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task, you may use it to find specific content, notes, headings, connections between notes, tags, or any text-based information across the Obsidian vault. The results include context, so analyze the surrounding text to better understand the matches. Leverage the regex_search_files tool in combination with other tools for comprehensive analysis. For example, use it to find specific phrases or patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.`
|
return `- When using the regex_search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task, you may use it to find specific content, notes, headings, connections between notes, tags, or any text-based information across the Obsidian vault. The results include context, so analyze the surrounding text to better understand the matches. Leverage the regex_search_files tool in combination with other tools for comprehensive analysis. For example, use it to find specific phrases or patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.`
|
||||||
} else if (searchTool === 'semantic') {
|
} else if (searchTool === 'semantic') {
|
||||||
return `- When using the semantic_search_files tool, craft your natural language query to describe concepts and ideas rather than specific patterns. Based on the user's task, you may use it to find thematically related content, conceptually similar notes, or knowledge connections across the Obsidian vault, even when exact keywords aren't present. The results include context, so analyze the surrounding text to understand the conceptual relevance of each match. Leverage the semantic_search_files tool in combination with other tools for comprehensive analysis. For example, use it to find specific phrases or patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.`
|
return `- When using the semantic_search_files tool, craft your natural language query to describe concepts and ideas rather than specific patterns. Based on the user's task, you may use it to find thematically related content, conceptually similar notes, or knowledge connections across the Obsidian vault, even when exact keywords aren't present. The results include context, so analyze the surrounding text to understand the conceptual relevance of each match. Leverage the semantic_search_files tool in combination with other tools for comprehensive analysis. For example, use it to find specific phrases or patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.`
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import * as path from 'path'
|
|||||||
|
|
||||||
import { App, normalizePath } from 'obsidian'
|
import { App, normalizePath } from 'obsidian'
|
||||||
|
|
||||||
|
import { FilesSearchSettings } from "../../types/settings"
|
||||||
import {
|
import {
|
||||||
CustomModePrompts,
|
CustomModePrompts,
|
||||||
Mode,
|
Mode,
|
||||||
@ -68,6 +69,7 @@ export class SystemPrompt {
|
|||||||
cwd: string,
|
cwd: string,
|
||||||
supportsComputerUse: boolean,
|
supportsComputerUse: boolean,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
|
searchSettings: FilesSearchSettings,
|
||||||
filesSearchMethod: string,
|
filesSearchMethod: string,
|
||||||
mcpHub?: McpHub,
|
mcpHub?: McpHub,
|
||||||
diffStrategy?: DiffStrategy,
|
diffStrategy?: DiffStrategy,
|
||||||
@ -105,6 +107,7 @@ ${getSharedToolUseSection()}
|
|||||||
${getToolDescriptionsForMode(
|
${getToolDescriptionsForMode(
|
||||||
mode,
|
mode,
|
||||||
cwd,
|
cwd,
|
||||||
|
searchSettings,
|
||||||
filesSearchMethod,
|
filesSearchMethod,
|
||||||
supportsComputerUse,
|
supportsComputerUse,
|
||||||
diffStrategy,
|
diffStrategy,
|
||||||
@ -148,6 +151,7 @@ ${await addCustomInstructions(this.app, promptComponent?.customInstructions || m
|
|||||||
cwd: string,
|
cwd: string,
|
||||||
supportsComputerUse: boolean,
|
supportsComputerUse: boolean,
|
||||||
mode: Mode = defaultModeSlug,
|
mode: Mode = defaultModeSlug,
|
||||||
|
searchSettings: FilesSearchSettings,
|
||||||
filesSearchMethod: string = 'regex',
|
filesSearchMethod: string = 'regex',
|
||||||
preferredLanguage?: string,
|
preferredLanguage?: string,
|
||||||
diffStrategy?: DiffStrategy,
|
diffStrategy?: DiffStrategy,
|
||||||
@ -203,6 +207,7 @@ ${customInstructions}`
|
|||||||
cwd,
|
cwd,
|
||||||
supportsComputerUse,
|
supportsComputerUse,
|
||||||
currentMode.slug,
|
currentMode.slug,
|
||||||
|
searchSettings,
|
||||||
filesSearchMethod,
|
filesSearchMethod,
|
||||||
mcpHub,
|
mcpHub,
|
||||||
diffStrategy,
|
diffStrategy,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Mode, ModeConfig, getGroupName, getModeConfig, isToolAllowedForMode } from "../../../utils/modes"
|
import { Mode, ModeConfig, getGroupName, getModeConfig, isToolAllowedForMode } from "../../../utils/modes"
|
||||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||||
import { McpHub } from "../../mcp/McpHub"
|
import { McpHub } from "../../mcp/McpHub"
|
||||||
|
import { FilesSearchSettings } from "../../../types/settings"
|
||||||
|
|
||||||
import { getAccessMcpResourceDescription } from "./access-mcp-resource"
|
import { getAccessMcpResourceDescription } from "./access-mcp-resource"
|
||||||
import { getAskFollowupQuestionDescription } from "./ask-followup-question"
|
import { getAskFollowupQuestionDescription } from "./ask-followup-question"
|
||||||
@ -40,6 +41,7 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
|
|||||||
export function getToolDescriptionsForMode(
|
export function getToolDescriptionsForMode(
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
cwd: string,
|
cwd: string,
|
||||||
|
searchSettings: FilesSearchSettings,
|
||||||
searchTool: string,
|
searchTool: string,
|
||||||
supportsComputerUse: boolean,
|
supportsComputerUse: boolean,
|
||||||
diffStrategy?: DiffStrategy,
|
diffStrategy?: DiffStrategy,
|
||||||
@ -51,6 +53,7 @@ export function getToolDescriptionsForMode(
|
|||||||
const config = getModeConfig(mode, customModes)
|
const config = getModeConfig(mode, customModes)
|
||||||
const args: ToolArgs = {
|
const args: ToolArgs = {
|
||||||
cwd,
|
cwd,
|
||||||
|
searchSettings,
|
||||||
searchTool,
|
searchTool,
|
||||||
supportsComputerUse,
|
supportsComputerUse,
|
||||||
diffStrategy,
|
diffStrategy,
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { ToolArgs } from "./types"
|
import { ToolArgs } from "./types"
|
||||||
|
|
||||||
export function getSearchFilesDescription(args: ToolArgs): string {
|
export function getSearchFilesDescription(args: ToolArgs): string {
|
||||||
if (args.searchTool === 'regex') {
|
if (args.searchTool === 'match') {
|
||||||
|
return getMatchSearchFilesDescription(args)
|
||||||
|
} else if (args.searchTool === 'regex') {
|
||||||
return getRegexSearchFilesDescription(args)
|
return getRegexSearchFilesDescription(args)
|
||||||
} else if (args.searchTool === 'semantic') {
|
} else if (args.searchTool === 'semantic') {
|
||||||
return getSemanticSearchFilesDescription(args)
|
return getSemanticSearchFilesDescription(args)
|
||||||
@ -10,12 +12,44 @@ export function getSearchFilesDescription(args: ToolArgs): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMatchSearchFilesDescription(args: ToolArgs): string {
|
||||||
|
return `## match_search_files
|
||||||
|
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.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
<match_search_files>
|
||||||
|
<path>Directory path here</path>
|
||||||
|
<query>Your keyword/phrase here</query>
|
||||||
|
</match_search_files>
|
||||||
|
|
||||||
|
Example: Requesting to search for all Markdown files containing 'test' in the current directory
|
||||||
|
<match_search_files>
|
||||||
|
<path>.</path>
|
||||||
|
<query>test</query>
|
||||||
|
</match_search_files>`
|
||||||
|
}
|
||||||
|
|
||||||
export function getRegexSearchFilesDescription(args: ToolArgs): string {
|
export function getRegexSearchFilesDescription(args: ToolArgs): string {
|
||||||
|
let regex_syntax: string;
|
||||||
|
switch (args.searchSettings.regexBackend) {
|
||||||
|
case 'coreplugin':
|
||||||
|
regex_syntax = "ECMAScript (JavaScript)";
|
||||||
|
break;
|
||||||
|
case 'ripgrep':
|
||||||
|
regex_syntax = "Rust";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
regex_syntax = "ECMAScript (JavaScript)";
|
||||||
|
}
|
||||||
|
|
||||||
return `## regex_search_files
|
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.
|
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:
|
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.
|
- 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:
|
Usage:
|
||||||
<regex_search_files>
|
<regex_search_files>
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
|
import { FilesSearchSettings } from "../../../types/settings"
|
||||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||||
import { McpHub } from "../../mcp/McpHub"
|
import { McpHub } from "../../mcp/McpHub"
|
||||||
|
|
||||||
export type ToolArgs = {
|
export type ToolArgs = {
|
||||||
cwd: string
|
cwd: string
|
||||||
|
searchSettings: FilesSearchSettings,
|
||||||
searchTool?: string,
|
searchTool?: string,
|
||||||
supportsComputerUse: boolean
|
supportsComputerUse: boolean
|
||||||
diffStrategy?: DiffStrategy
|
diffStrategy?: DiffStrategy
|
||||||
|
|||||||
@ -81,6 +81,7 @@ export default {
|
|||||||
copy: "Copy",
|
copy: "Copy",
|
||||||
editOrApplyDiff: "{mode}: {path}",
|
editOrApplyDiff: "{mode}: {path}",
|
||||||
loading: "Loading...",
|
loading: "Loading...",
|
||||||
|
matchSearchInPath: 'match search files "{query}" in {path}',
|
||||||
regexSearchInPath: 'regex search files "{regex}" in {path}',
|
regexSearchInPath: 'regex search files "{regex}" in {path}',
|
||||||
createNewNote: "Create new note",
|
createNewNote: "Create new note",
|
||||||
copyMsg: "Copy message",
|
copyMsg: "Copy message",
|
||||||
@ -227,8 +228,16 @@ export default {
|
|||||||
auto: 'Auto',
|
auto: 'Auto',
|
||||||
semantic: 'Semantic',
|
semantic: 'Semantic',
|
||||||
regex: 'Regex',
|
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',
|
||||||
|
coreplugin: 'Core plugin',
|
||||||
|
omnisearch: 'Omnisearch',
|
||||||
ripgrepPath: 'ripgrep path',
|
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
|
// Chat Behavior Section
|
||||||
|
|||||||
@ -82,6 +82,7 @@ export default {
|
|||||||
copy: "复制",
|
copy: "复制",
|
||||||
editOrApplyDiff: "{mode}:{path}",
|
editOrApplyDiff: "{mode}:{path}",
|
||||||
loading: "加载中...",
|
loading: "加载中...",
|
||||||
|
matchSearchInPath: '在 {path} 中匹配搜索文件 "{query}"',
|
||||||
regexSearchInPath: '在 {path} 中正则搜索文件 "{regex}"',
|
regexSearchInPath: '在 {path} 中正则搜索文件 "{regex}"',
|
||||||
createNewNote: "创建新笔记",
|
createNewNote: "创建新笔记",
|
||||||
copyMsg: "复制消息",
|
copyMsg: "复制消息",
|
||||||
@ -228,8 +229,16 @@ export default {
|
|||||||
auto: '自动',
|
auto: '自动',
|
||||||
semantic: '语义',
|
semantic: '语义',
|
||||||
regex: '正则',
|
regex: '正则',
|
||||||
|
match: '匹配',
|
||||||
|
regexBackend: '正则搜索后端',
|
||||||
|
regexBackendDescription: '选择正则搜索的后端。',
|
||||||
|
matchBackend: '匹配搜索后端',
|
||||||
|
matchBackendDescription: '选择匹配搜索的后端。',
|
||||||
|
ripgrep: 'ripgrep',
|
||||||
|
coreplugin: '核心插件',
|
||||||
|
omnisearch: 'Omnisearch',
|
||||||
ripgrepPath: 'ripgrep 路径',
|
ripgrepPath: 'ripgrep 路径',
|
||||||
ripgrepPathDescription: 'ripgrep 二进制文件的路径。使用正则搜索时需要此项。',
|
ripgrepPathDescription: 'ripgrep 二进制文件的路径。使用 ripgrep 正则搜索时需要此项。',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 聊天行为部分
|
// 聊天行为部分
|
||||||
|
|||||||
@ -155,11 +155,51 @@ export class InfioSettingTab extends PluginSettingTab {
|
|||||||
.addOption('auto', t('settings.FilesSearch.auto'))
|
.addOption('auto', t('settings.FilesSearch.auto'))
|
||||||
.addOption('semantic', t('settings.FilesSearch.semantic'))
|
.addOption('semantic', t('settings.FilesSearch.semantic'))
|
||||||
.addOption('regex', t('settings.FilesSearch.regex'))
|
.addOption('regex', t('settings.FilesSearch.regex'))
|
||||||
.setValue(this.plugin.settings.filesSearchMethod)
|
.addOption('match', t('settings.FilesSearch.match'))
|
||||||
|
.setValue(this.plugin.settings.filesSearchSettings.method)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
await this.plugin.setSettings({
|
await this.plugin.setSettings({
|
||||||
...this.plugin.settings,
|
...this.plugin.settings,
|
||||||
filesSearchMethod: value as 'regex' | 'semantic' | 'auto',
|
filesSearchSettings: {
|
||||||
|
...this.plugin.settings.filesSearchSettings,
|
||||||
|
method: value as 'match' | 'regex' | 'semantic' | 'auto',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(t('settings.FilesSearch.regexBackend'))
|
||||||
|
.setDesc(t('settings.FilesSearch.regexBackendDescription'))
|
||||||
|
.addDropdown((dropdown) =>
|
||||||
|
dropdown
|
||||||
|
.addOption('ripgrep', t('settings.FilesSearch.ripgrep'))
|
||||||
|
.addOption('coreplugin', t('settings.FilesSearch.coreplugin'))
|
||||||
|
.setValue(this.plugin.settings.filesSearchSettings.regexBackend)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
await this.plugin.setSettings({
|
||||||
|
...this.plugin.settings,
|
||||||
|
filesSearchSettings: {
|
||||||
|
...this.plugin.settings.filesSearchSettings,
|
||||||
|
regexBackend: 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.filesSearchSettings.matchBackend)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
await this.plugin.setSettings({
|
||||||
|
...this.plugin.settings,
|
||||||
|
filesSearchSettings: {
|
||||||
|
...this.plugin.settings.filesSearchSettings,
|
||||||
|
matchBackend: value as 'coreplugin' | 'omnisearch',
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -169,11 +209,14 @@ export class InfioSettingTab extends PluginSettingTab {
|
|||||||
.addText((text) =>
|
.addText((text) =>
|
||||||
text
|
text
|
||||||
.setPlaceholder('/opt/homebrew/bin/')
|
.setPlaceholder('/opt/homebrew/bin/')
|
||||||
.setValue(this.plugin.settings.ripgrepPath)
|
.setValue(this.plugin.settings.filesSearchSettings.ripgrepPath)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
await this.plugin.setSettings({
|
await this.plugin.setSettings({
|
||||||
...this.plugin.settings,
|
...this.plugin.settings,
|
||||||
|
filesSearchSettings: {
|
||||||
|
...this.plugin.settings.filesSearchSettings,
|
||||||
ripgrepPath: value,
|
ripgrepPath: value,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -20,6 +20,14 @@ export type ListFilesToolArgs = {
|
|||||||
recursive?: boolean;
|
recursive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MatchSearchFilesToolArgs = {
|
||||||
|
type: 'match_search_files';
|
||||||
|
filepath?: string;
|
||||||
|
query?: string;
|
||||||
|
file_pattern?: string;
|
||||||
|
finish?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export type RegexSearchFilesToolArgs = {
|
export type RegexSearchFilesToolArgs = {
|
||||||
type: 'regex_search_files';
|
type: 'regex_search_files';
|
||||||
filepath?: string;
|
filepath?: string;
|
||||||
@ -97,4 +105,4 @@ export type UseMcpToolArgs = {
|
|||||||
parameters: Record<string, unknown>;
|
parameters: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@ -12,7 +12,12 @@ describe('parseSmartCopilotSettings', () => {
|
|||||||
infioApiKey: '',
|
infioApiKey: '',
|
||||||
openAIApiKey: '',
|
openAIApiKey: '',
|
||||||
anthropicApiKey: '',
|
anthropicApiKey: '',
|
||||||
filesSearchMethod: 'auto',
|
filesSearchSettings: {
|
||||||
|
method: 'auto',
|
||||||
|
regexBackend: 'ripgrep',
|
||||||
|
matchBackend: 'coreplugin',
|
||||||
|
ripgrepPath: '',
|
||||||
|
},
|
||||||
fuzzyMatchThreshold: 0.85,
|
fuzzyMatchThreshold: 0.85,
|
||||||
geminiApiKey: '',
|
geminiApiKey: '',
|
||||||
groqApiKey: '',
|
groqApiKey: '',
|
||||||
@ -98,7 +103,6 @@ describe('parseSmartCopilotSettings', () => {
|
|||||||
defaultMention: 'none',
|
defaultMention: 'none',
|
||||||
removeDuplicateMathBlockIndicator: true,
|
removeDuplicateMathBlockIndicator: true,
|
||||||
removeDuplicateCodeBlockIndicator: true,
|
removeDuplicateCodeBlockIndicator: true,
|
||||||
ripgrepPath: '',
|
|
||||||
serperApiKey: '',
|
serperApiKey: '',
|
||||||
serperSearchEngine: 'google',
|
serperSearchEngine: 'google',
|
||||||
ignoredFilePatterns: '**/secret/**\n',
|
ignoredFilePatterns: '**/secret/**\n',
|
||||||
@ -195,7 +199,12 @@ describe('settings migration', () => {
|
|||||||
infioApiKey: '',
|
infioApiKey: '',
|
||||||
openAIApiKey: '',
|
openAIApiKey: '',
|
||||||
anthropicApiKey: '',
|
anthropicApiKey: '',
|
||||||
filesSearchMethod: 'auto',
|
filesSearchSettings: {
|
||||||
|
method: 'auto',
|
||||||
|
regexBackend: 'ripgrep',
|
||||||
|
matchBackend: 'coreplugin',
|
||||||
|
ripgrepPath: '',
|
||||||
|
},
|
||||||
fuzzyMatchThreshold: 0.85,
|
fuzzyMatchThreshold: 0.85,
|
||||||
geminiApiKey: '',
|
geminiApiKey: '',
|
||||||
groqApiKey: '',
|
groqApiKey: '',
|
||||||
@ -281,7 +290,6 @@ describe('settings migration', () => {
|
|||||||
defaultMention: 'none',
|
defaultMention: 'none',
|
||||||
removeDuplicateMathBlockIndicator: true,
|
removeDuplicateMathBlockIndicator: true,
|
||||||
removeDuplicateCodeBlockIndicator: true,
|
removeDuplicateCodeBlockIndicator: true,
|
||||||
ripgrepPath: '',
|
|
||||||
serperApiKey: '',
|
serperApiKey: '',
|
||||||
serperSearchEngine: 'google',
|
serperSearchEngine: 'google',
|
||||||
ignoredFilePatterns: '**/secret/**\n',
|
ignoredFilePatterns: '**/secret/**\n',
|
||||||
|
|||||||
@ -201,6 +201,18 @@ export const triggerSchema = z.object({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const FilesSearchSettingsSchema = z.object({
|
||||||
|
method: z.enum(['match', 'regex', 'semantic', 'auto']).catch('auto'),
|
||||||
|
regexBackend: z.enum(['coreplugin', 'ripgrep']).catch('ripgrep'),
|
||||||
|
matchBackend: z.enum(['omnisearch', 'coreplugin']).catch('coreplugin'),
|
||||||
|
ripgrepPath: z.string().catch(''),
|
||||||
|
}).catch({
|
||||||
|
method: 'auto',
|
||||||
|
regexBackend: 'ripgrep',
|
||||||
|
matchBackend: 'coreplugin',
|
||||||
|
ripgrepPath: '',
|
||||||
|
});
|
||||||
|
|
||||||
export const InfioSettingsSchema = z.object({
|
export const InfioSettingsSchema = z.object({
|
||||||
// Version
|
// Version
|
||||||
version: z.literal(SETTINGS_SCHEMA_VERSION).catch(SETTINGS_SCHEMA_VERSION),
|
version: z.literal(SETTINGS_SCHEMA_VERSION).catch(SETTINGS_SCHEMA_VERSION),
|
||||||
@ -260,8 +272,7 @@ export const InfioSettingsSchema = z.object({
|
|||||||
jinaApiKey: z.string().catch(''),
|
jinaApiKey: z.string().catch(''),
|
||||||
|
|
||||||
// Files Search
|
// Files Search
|
||||||
filesSearchMethod: z.enum(['regex', 'semantic', 'auto']).catch('auto'),
|
filesSearchSettings: FilesSearchSettingsSchema,
|
||||||
ripgrepPath: z.string().catch(''),
|
|
||||||
|
|
||||||
/// [compatible]
|
/// [compatible]
|
||||||
// activeModels [compatible]
|
// activeModels [compatible]
|
||||||
@ -363,6 +374,7 @@ export const InfioSettingsSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export type InfioSettings = z.infer<typeof InfioSettingsSchema>
|
export type InfioSettings = z.infer<typeof InfioSettingsSchema>
|
||||||
|
export type FilesSearchSettings = z.infer<typeof FilesSearchSettingsSchema>
|
||||||
|
|
||||||
type Migration = {
|
type Migration = {
|
||||||
fromVersion: number
|
fromVersion: number
|
||||||
|
|||||||
@ -28,6 +28,10 @@ export const listFilesAndFolders = async (vault: Vault, path: string) => {
|
|||||||
return []
|
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) => {
|
export const regexSearchFiles = async (vault: Vault, path: string, regex: string, file_pattern: string) => {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,6 +59,11 @@ export type ParsedMsgBlock =
|
|||||||
path: string
|
path: string
|
||||||
recursive?: boolean
|
recursive?: boolean
|
||||||
finish: boolean
|
finish: boolean
|
||||||
|
} | {
|
||||||
|
type: 'match_search_files'
|
||||||
|
path: string
|
||||||
|
query: string
|
||||||
|
finish: boolean
|
||||||
} | {
|
} | {
|
||||||
type: 'regex_search_files'
|
type: 'regex_search_files'
|
||||||
path: string
|
path: string
|
||||||
@ -226,6 +231,36 @@ export function parseMsgBlocks(
|
|||||||
finish: node.sourceCodeLocation.endTag !== undefined
|
finish: node.sourceCodeLocation.endTag !== undefined
|
||||||
})
|
})
|
||||||
lastEndOffset = endOffset
|
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') {
|
} else if (node.nodeName === 'regex_search_files') {
|
||||||
if (!node.sourceCodeLocation) {
|
if (!node.sourceCodeLocation) {
|
||||||
throw new Error('sourceCodeLocation is undefined')
|
throw new Error('sourceCodeLocation is undefined')
|
||||||
|
|||||||
@ -182,7 +182,7 @@ export class PromptGenerator {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
let filesSearchMethod = this.settings.filesSearchMethod
|
let filesSearchMethod = this.settings.filesSearchSettings.method
|
||||||
if (filesSearchMethod === 'auto' && this.settings.embeddingModelId && this.settings.embeddingModelId !== '') {
|
if (filesSearchMethod === 'auto' && this.settings.embeddingModelId && this.settings.embeddingModelId !== '') {
|
||||||
filesSearchMethod = 'semantic'
|
filesSearchMethod = 'semantic'
|
||||||
}
|
}
|
||||||
@ -520,6 +520,7 @@ export class PromptGenerator {
|
|||||||
this.app.vault.getRoot().path,
|
this.app.vault.getRoot().path,
|
||||||
false,
|
false,
|
||||||
mode,
|
mode,
|
||||||
|
this.settings.filesSearchSettings,
|
||||||
filesSearchMethod,
|
filesSearchMethod,
|
||||||
preferredLanguage,
|
preferredLanguage,
|
||||||
this.diffStrategy,
|
this.diffStrategy,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user