237 lines
7.9 KiB
TypeScript
237 lines
7.9 KiB
TypeScript
/**
|
||
* Управляет автокомплитом для ссылок на файлы и промпты.
|
||
* Обеспечивает поиск файлов в хранилище и кеширование результатов.
|
||
*/
|
||
|
||
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;
|
||
}
|
||
}
|