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.
This commit is contained in:
travertexg 2025-06-09 16:46:38 +00:00
parent 9984527e85
commit c2dfb48e22
7 changed files with 90 additions and 85 deletions

View File

@ -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<string> {
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<any[]>((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.";
}
}

View File

@ -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);

View File

@ -12,6 +12,6 @@ export async function regexSearchFilesWithCorePlugin(
regex: string,
app: App,
): Promise<string> {
const query = "/" + regex + "/";
return searchFilesWithCorePlugin(query, app);
const regexQuery = `/${regex}/`;
return searchFilesWithCorePlugin(regexQuery, app);
}

View File

@ -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<SearchResult> | null = null

View File

@ -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

View File

@ -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.',
},

View File

@ -236,6 +236,7 @@ export default {
matchBackendDescription: '选择匹配搜索的后端。',
ripgrep: 'ripgrep',
coreplugin: '核心插件',
omnisearch: 'Omnisearch',
ripgrepPath: 'ripgrep 路径',
ripgrepPathDescription: 'ripgrep 二进制文件的路径。使用 ripgrep 正则搜索时需要此项。',
},