llm-agent-plugin/src/references/AutocompleteManager.ts

237 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Управляет автокомплитом для ссылок на файлы и промпты.
* Обеспечивает поиск файлов в хранилище и кеширование результатов.
*/
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<string, AutocompleteItem[]> = new Map();
private promptCache: Map<string, AutocompleteItem[]> = 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<AutocompleteItem[]> {
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<AutocompleteItem[]> {
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<void> {
const now = Date.now();
if (now - this.lastCacheUpdate > this.CACHE_TTL) {
await this.updateCache();
this.lastCacheUpdate = now;
}
}
/**
* Обновляет кеш файлов и промптов
*/
private async updateCache(): Promise<void> {
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;
}
}