/** * Управляет автокомплитом для ссылок на файлы и промпты. * Обеспечивает поиск файлов в хранилище и кеширование результатов. */ import { TFile } from 'obsidian'; import LLMAgentPlugin from 'main'; // ----------------------------------------------- Interfaces --------------------------------------------------------------- export interface AutocompleteItem { name: string; path: string; type: 'file' | 'prompt'; icon: string; // Obsidian icon name file?: TFile; // Ссылка на файл Obsidian } // ----------------------------------------------- Autocomplete Manager --------------------------------------------------------------- export class AutocompleteManager { private plugin: LLMAgentPlugin; private fileCache: Map = new Map(); private promptCache: Map = new Map(); private lastCacheUpdate = 0; private readonly CACHE_TTL = 30000; // 30 секунд constructor(plugin: LLMAgentPlugin) { this.plugin = plugin; this.setupFileWatcher(); // Слушаем изменения настроек для очистки кеша this.plugin.eventBus.on('settings-changed', this.clearCache.bind(this)); } // ----------------------------------------------- Public Methods --------------------------------------------------------------- /** * Получает список файлов для автокомплита * @param query - поисковый запрос * @returns массив файлов для автокомплита */ async getFileOptions(query: string): Promise { if (!this.plugin.settings.enableFileReferences) { return []; } await this.updateCacheIfNeeded(); const allFiles = this.fileCache.get('all') || []; if (!query.trim()) { return allFiles.slice(0, 10); // Показываем первые 10 файлов } const lowercaseQuery = query.toLowerCase(); return allFiles .filter((item) => item.name.toLowerCase().includes(lowercaseQuery) || item.path.toLowerCase().includes(lowercaseQuery)) .slice(0, 10); } /** * Получает список промптов для автокомплита * @param query - поисковый запрос * @returns массив промптов для автокомплита */ async getPromptOptions(query: string): Promise { if (!this.plugin.settings.enablePromptReferences) { return []; } await this.updateCacheIfNeeded(); const allPrompts = this.promptCache.get('all') || []; if (!query.trim()) { return allPrompts.slice(0, 10); } const lowercaseQuery = query.toLowerCase(); return allPrompts .filter((item) => item.name.toLowerCase().includes(lowercaseQuery) || item.path.toLowerCase().includes(lowercaseQuery)) .slice(0, 10); } /** * Очищает кеш (используется при изменении настроек) */ clearCache(): void { this.fileCache.clear(); this.promptCache.clear(); this.lastCacheUpdate = 0; } // ----------------------------------------------- Private Methods --------------------------------------------------------------- /** * Обновляет кеш, если он устарел */ private async updateCacheIfNeeded(): Promise { const now = Date.now(); if (now - this.lastCacheUpdate > this.CACHE_TTL) { await this.updateCache(); this.lastCacheUpdate = now; } } /** * Обновляет кеш файлов и промптов */ private async updateCache(): Promise { const files = this.plugin.app.vault.getMarkdownFiles(); const fileItems: AutocompleteItem[] = []; const promptItems: AutocompleteItem[] = []; // Нормализуем пути исключенных папок const normalizedExcludedFolders = this.plugin.settings.excludedFolders .filter(folder => folder.trim() !== '') .map(folder => folder.endsWith('/') ? folder : folder + '/') // Убедимся, что заканчивается на слэш .map(folder => folder.startsWith('/') ? folder.substring(1) : folder); // Удаляем начальный слэш for (const file of files) { // Пропускаем файлы из исключенных папок if (this.isFileInExcludedFolder(file, normalizedExcludedFolders)) { continue; } const item: AutocompleteItem = { name: file.basename, path: file.path, type: this.isPromptFile(file) ? 'prompt' : 'file', icon: this.getFileIcon(file), file: file }; if (item.type === 'prompt') { promptItems.push(item); } else { fileItems.push(item); } } // Сортируем по имени fileItems.sort((a, b) => a.name.localeCompare(b.name)); promptItems.sort((a, b) => a.name.localeCompare(b.name)); this.fileCache.set('all', fileItems); this.promptCache.set('all', promptItems); } /** * Проверяет, является ли файл промптом */ private isPromptFile(file: TFile): boolean { const promptsFolder = this.plugin.settings.promptsFolder; if (!promptsFolder) return false; // Нормализуем путь к папке промптов const normalizedPromptsPath = promptsFolder.replace(/^\/+|\/+$/g, ''); return file.path.startsWith(normalizedPromptsPath + '/') || file.path === normalizedPromptsPath; } /** * Возвращает иконку для файла на основе Obsidian иконок */ private getFileIcon(file: TFile): string { if (this.isPromptFile(file)) { return 'zap'; // Иконка промпта } // Определяем иконку по расширению const extension = file.extension.toLowerCase(); switch (extension) { case 'md': return 'file-text'; case 'js': case 'ts': return 'code'; case 'json': return 'brackets'; case 'css': return 'palette'; case 'png': case 'jpg': case 'jpeg': case 'gif': return 'image'; default: return 'file'; } } /** * Настраивает отслеживание изменений файлов для обновления кеша */ private setupFileWatcher(): void { this.plugin.registerEvent( this.plugin.app.vault.on('create', () => { this.clearCache(); }) ); this.plugin.registerEvent( this.plugin.app.vault.on('delete', () => { this.clearCache(); }) ); this.plugin.registerEvent( this.plugin.app.vault.on('rename', () => { this.clearCache(); }) ); } /** * Проверяет, находится ли файл в одной из исключенных папок. * @param file - файл Obsidian. * @param excludedFolders - массив нормализованных путей исключенных папок. * @returns true, если файл находится в исключенной папке. */ private isFileInExcludedFolder(file: TFile, excludedFolders: string[]): boolean { const filePath = file.path.toLowerCase(); // Для регистронезависимого сравнения for (const folder of excludedFolders) { const normalizedFolder = folder.toLowerCase(); // Проверяем, начинается ли путь файла с пути исключенной папки // Или если файл находится прямо в корне исключенной папки (н.п. "folder/file.md") if (filePath.startsWith(normalizedFolder) ) { return true; } } return false; } }