From cfb3a50eac371013abaff14575802fe118da9d9d Mon Sep 17 00:00:00 2001 From: Bill Date: Sun, 19 Oct 2025 23:41:35 -0400 Subject: [PATCH] refactor: migrate search and getVaultInfo to use adapters Update search and getVaultInfo methods to use IVaultAdapter instead of direct App.vault access. Implements inline search logic using adapters for file reading and markdown file listing. --- src/tools/vault-tools.ts | 204 +++++++++++++++++++++++++++++++-------- 1 file changed, 164 insertions(+), 40 deletions(-) diff --git a/src/tools/vault-tools.ts b/src/tools/vault-tools.ts index 9fd300d..4dc6a93 100644 --- a/src/tools/vault-tools.ts +++ b/src/tools/vault-tools.ts @@ -16,33 +16,48 @@ export class VaultTools { ) {} async getVaultInfo(): Promise { - const files = this.app.vault.getFiles(); - const markdownFiles = this.app.vault.getMarkdownFiles(); - const folders = this.app.vault.getAllLoadedFiles().filter(f => f instanceof TFolder); - - // Calculate total size - let totalSize = 0; - for (const file of files) { - if (file instanceof TFile) { - totalSize += file.stat.size; - } - } - - const info: VaultInfo = { - name: this.app.vault.getName(), - path: (this.app.vault.adapter as any).basePath || 'Unknown', - totalFiles: files.length, - totalFolders: folders.length, - markdownFiles: markdownFiles.length, - totalSize: totalSize - }; + try { + const allFiles = this.vault.getMarkdownFiles(); + const totalNotes = allFiles.length; - return { - content: [{ - type: "text", - text: JSON.stringify(info, null, 2) - }] - }; + // Calculate total size + let totalSize = 0; + for (const file of allFiles) { + const stat = this.vault.stat(file); + if (stat) { + totalSize += stat.size; + } + } + + const info = { + totalNotes, + totalSize, + sizeFormatted: this.formatBytes(totalSize) + }; + + return { + content: [{ + type: "text", + text: JSON.stringify(info, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: "text", + text: `Get vault info error: ${(error as Error).message}` + }], + isError: true + }; + } + } + + private formatBytes(bytes: number): string { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; } async listNotes(path?: string): Promise { @@ -544,25 +559,134 @@ export class VaultTools { } = options; try { - const { matches, stats } = await SearchUtils.search(this.app, { - query, - isRegex, - caseSensitive, - includes, - excludes, - folder, - returnSnippets, - snippetLength, - maxResults - }); + // Compile search pattern + let searchPattern: RegExp; + try { + if (isRegex) { + const flags = caseSensitive ? 'g' : 'gi'; + searchPattern = new RegExp(query, flags); + } else { + // Escape special regex characters for literal search + const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const flags = caseSensitive ? 'g' : 'gi'; + searchPattern = new RegExp(escapedQuery, flags); + } + } catch (error) { + return { + content: [{ + type: "text", + text: `Invalid regex pattern: ${(error as Error).message}` + }], + isError: true + }; + } + + // Get files to search using adapter + let files = this.vault.getMarkdownFiles(); + + // Filter by folder if specified + if (folder) { + const folderPath = folder.endsWith('/') ? folder : folder + '/'; + files = files.filter(file => + file.path.startsWith(folderPath) || file.path === folder + ); + } + + // Apply glob filtering + if (includes || excludes) { + files = files.filter(file => + GlobUtils.shouldInclude(file.path, includes, excludes) + ); + } + + const matches: SearchMatch[] = []; + const filesWithMatches = new Set(); + let filesSearched = 0; + + // Search through files + for (const file of files) { + if (matches.length >= maxResults) { + break; + } + + filesSearched++; + + try { + const content = await this.vault.read(file); + const lines = content.split('\n'); + + // Search in content + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + if (matches.length >= maxResults) { + break; + } + + const line = lines[lineIndex]; + + // Reset regex lastIndex for global patterns + searchPattern.lastIndex = 0; + + let match: RegExpExecArray | null; + while ((match = searchPattern.exec(line)) !== null) { + if (matches.length >= maxResults) { + break; + } + + const columnIndex = match.index; + const matchText = match[0]; + + // Extract snippet with context + let snippet = line; + let snippetStart = 0; + let matchStart = columnIndex; + + if (returnSnippets && line.length > snippetLength) { + // Calculate snippet boundaries + const halfSnippet = Math.floor(snippetLength / 2); + snippetStart = Math.max(0, columnIndex - halfSnippet); + const snippetEnd = Math.min(line.length, snippetStart + snippetLength); + + // Adjust if we're at the end of the line + if (snippetEnd === line.length && line.length > snippetLength) { + snippetStart = Math.max(0, line.length - snippetLength); + } + + snippet = line.substring(snippetStart, snippetEnd); + matchStart = columnIndex - snippetStart; + } + + matches.push({ + path: file.path, + line: lineIndex + 1, // 1-indexed + column: columnIndex + 1, // 1-indexed + snippet: snippet, + matchRanges: [{ + start: matchStart, + end: matchStart + matchText.length + }] + }); + + filesWithMatches.add(file.path); + + // Prevent infinite loop for zero-width matches + if (match[0].length === 0) { + searchPattern.lastIndex++; + } + } + } + } catch (error) { + // Skip files that can't be read + console.error(`Failed to search file ${file.path}:`, error); + } + } const result: SearchResult = { query, isRegex, matches, - totalMatches: stats.totalMatches, - filesSearched: stats.filesSearched, - filesWithMatches: stats.filesWithMatches + totalMatches: matches.length, + filesSearched, + filesWithMatches: filesWithMatches.size }; return {