andy-stack_vaultkeeper-ai/Services/VaultCacheService.ts
Andrew Beal 9cf8f1dbd7 refactor: move replaceCopy helper to Helpers module and add tag merge utilities
Move replaceCopy function from Copy enum to Helpers module to improve
separation of concerns. Add splitFrontmatter and mergeTagsIntoFrontmatter
utilities for YAML frontmatter manipulation. Remove unused Beautify prompt
and QuickActionsService. Add timeout support to Semaphore.wait(). Remove
unused Event.QuickActionsSettingsChanged. Update all imports across AI
classes, prompts, services, and tests.
2026-05-29 22:01:02 +01:00

195 lines
No EOL
6.4 KiB
TypeScript

import type VaultkeeperAIPlugin from "main";
import { Resolve } from "./DependencyService";
import { Services } from "./Services";
import type { VaultService } from "./VaultService";
import { FileEvent } from "Enums/FileEvent";
import { getAllTags, MetadataCache, TFile, TFolder } from "obsidian";
import { FileTagMapping } from "Helpers/FileTagMapping";
import * as fuzzysort from "fuzzysort";
import { Path } from "Enums/Path";
import { WikiLinks } from "Helpers/WikiLinks";
// Note that 'files' actually refers to both directories and files (Obsidian naming)
export class VaultCacheService {
public tags: Set<string> = new Set();
public wikiLinks: WikiLinks = new WikiLinks();
private readonly fuzzysortOptions = {
limit: 10,
all: false,
key: "prepared"
};
private readonly plugin: VaultkeeperAIPlugin;
private readonly vaultService: VaultService;
private readonly metaDataCache: MetadataCache;
private files: Map<string, TFile> = new Map();
private folders: Map<string, TFolder> = new Map();
private mapping: FileTagMapping = new FileTagMapping();
private preparedTags: { prepared: Fuzzysort.Prepared, tag: string }[] = [];
private preparedFiles: { prepared: Fuzzysort.Prepared, file: TFile }[] = [];
private preparedFolders: { prepared: Fuzzysort.Prepared, folder: TFolder }[] = [];
private initialised = false;
public constructor() {
this.plugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
this.vaultService = Resolve<VaultService>(Services.VaultService);
this.metaDataCache = this.plugin.app.metadataCache;
this.registerFileEvents();
this.plugin.app.metadataCache.on("resolved", async () => {
if (!this.initialised) {
await this.setupCaches();
this.initialised = true;
}
});
}
public matchTag(input: string): Fuzzysort.KeyResults<{ prepared: Fuzzysort.Prepared, tag: string }> {
return fuzzysort.go(input.toLowerCase(), this.preparedTags, this.fuzzysortOptions);
}
public matchFile(input: string): Fuzzysort.KeyResults<{ prepared: Fuzzysort.Prepared, file: TFile }> {
return fuzzysort.go(input.toLowerCase(), this.preparedFiles, this.fuzzysortOptions);
}
public matchFolder(input: string): Fuzzysort.KeyResults<{ prepared: Fuzzysort.Prepared, folder: TFolder }> {
return fuzzysort.go(input.toLowerCase(), this.preparedFolders, this.fuzzysortOptions);
}
private registerFileEvents() {
this.vaultService.registerFileEvents((event, file, args) => {
const shouldCacheNewPath = this.shouldBeCached(file.path);
const shouldCacheOldPath = args.oldPath ? this.shouldBeCached(args.oldPath) : false;
if (!shouldCacheNewPath && !shouldCacheOldPath) {
return;
}
if (file instanceof TFile) {
switch (event) {
case FileEvent.Create:
if (shouldCacheNewPath) {
this.wikiLinks.addWikiLink(file);
this.files.set(file.path, file);
this.cacheTags(file);
}
break;
case FileEvent.Modify:
if (shouldCacheNewPath) {
const newTags = this.getTags(file);
const removedTags = this.mapping.updateMapping(file.path, newTags);
removedTags.forEach(tag => this.tags.delete(tag));
this.cacheTags(file, newTags);
}
break;
case FileEvent.Rename:
if (shouldCacheOldPath) {
this.wikiLinks.removeWikiLink(args.oldPath);
this.files.delete(args.oldPath);
const orphanedTags = this.mapping.deleteFromMapping(args.oldPath);
orphanedTags.forEach(tag => this.tags.delete(tag));
}
if (shouldCacheNewPath) {
this.wikiLinks.addWikiLink(file);
this.mapping.renameKey(args.oldPath, file.path);
this.files.set(file.path, file);
this.cacheTags(file);
}
break;
case FileEvent.Delete:
this.wikiLinks.removeWikiLink(file);
this.files.delete(file.path);
this.mapping.deleteFromMapping(file.path).forEach(tag => this.tags.delete(tag));
break;
}
this.fuzzySortPrepareTags();
this.fuzzySortPrepareFiles();
} else if (file instanceof TFolder) {
switch (event) {
case FileEvent.Create:
if (shouldCacheNewPath) {
this.folders.set(file.path, file);
}
break;
case FileEvent.Rename:
if (shouldCacheOldPath) {
this.folders.delete(args.oldPath);
}
if (shouldCacheNewPath) {
this.folders.set(file.path, file);
}
break;
case FileEvent.Delete:
this.folders.delete(file.path);
break;
case FileEvent.Modify:
break; // ignore modifications for folders
}
this.fuzzySortPrepareFolders();
}
});
}
private async setupCaches() {
(await this.vaultService.listDirectoryContents(Path.Root)).forEach(file => {
if (file instanceof TFile) {
this.wikiLinks.addWikiLink(file);
this.files.set(file.path, file);
this.cacheTags(file);
} else if (file instanceof TFolder) {
this.folders.set(file.path, file);
}
});
this.fuzzySortPrepareTags();
this.fuzzySortPrepareFiles();
this.fuzzySortPrepareFolders();
}
private cacheTags(file: TFile, fileTags?: string[]) {
const tags = fileTags ?? this.getTags(file);
tags.forEach(tag => this.tags.add(tag));
this.mapping.set(file.path, tags);
}
private getTags(file: TFile): string[] {
const metaData = this.metaDataCache.getCache(file.path);
return metaData ? (getAllTags(metaData) ?? []) : [];
}
private fuzzySortPrepareTags() {
this.preparedTags = [];
this.tags.forEach(tag => {
this.preparedTags.push({ prepared: fuzzysort.prepare(tag), tag: tag });
});
}
private fuzzySortPrepareFiles() {
this.preparedFiles = [];
this.files.forEach(file => {
this.preparedFiles.push({ prepared: fuzzysort.prepare(file.basename), file: file });
});
}
private fuzzySortPrepareFolders() {
this.preparedFolders = [];
this.folders.forEach(folder => {
this.preparedFolders.push({ prepared: fuzzysort.prepare(folder.path), folder: folder });
});
}
private shouldBeCached(path: string) {
return !this.vaultService.isExclusion(path, false);
}
}