mirror of
https://github.com/jacobtread/obsidian-timekeep.git
synced 2026-07-22 10:10:27 +00:00
feat: vault timekeep registry
This commit is contained in:
parent
87de57bde0
commit
b3dc28a7e8
6 changed files with 290 additions and 0 deletions
28
package-lock.json
generated
28
package-lock.json
generated
|
|
@ -10,6 +10,7 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"moment": "^2.30.1",
|
||||
"p-limit": "^7.3.0",
|
||||
"pdfmake": "^0.3.7",
|
||||
"uuid": "^11.0.5",
|
||||
"zod": "^4.3.6"
|
||||
|
|
@ -2210,6 +2211,21 @@
|
|||
"@oxlint-tsgolint/win32-x64": "0.17.4"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz",
|
||||
"integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"yocto-queue": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
|
|
@ -2736,6 +2752,18 @@
|
|||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
|
||||
"integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"moment": "^2.30.1",
|
||||
"p-limit": "^7.3.0",
|
||||
"pdfmake": "^0.3.7",
|
||||
"uuid": "^11.0.5",
|
||||
"zod": "^4.3.6"
|
||||
|
|
|
|||
17
src/main.ts
17
src/main.ts
|
|
@ -24,6 +24,7 @@ import { load, replaceTimekeepCodeblock, extractTimekeepCodeblocks } from "@/tim
|
|||
import { stopAllTimekeeps } from "./commands/stopAllTimekeeps";
|
||||
import { stopFileTimekeeps } from "./commands/stopFileTimekeeps";
|
||||
import { CustomOutputFormat } from "./output";
|
||||
import { TimekeepRegistry } from "./service/registry";
|
||||
import { Timekeep, TimeEntry } from "./timekeep/schema";
|
||||
import { TimekeepLocatorModal } from "./views/timekeep-locator-modal";
|
||||
import { TimekeepMarkdownView } from "./views/timekeep-markdown-view";
|
||||
|
|
@ -48,6 +49,9 @@ export default class TimekeepPlugin extends Plugin {
|
|||
stopAllTimekeeps: (vault: Vault, currentTime: Moment) => Promise<number>;
|
||||
stopFileTimekeeps: (vault: Vault, file: TFile, currentTime: Moment) => Promise<number>;
|
||||
|
||||
/** Registry of all timekeeps within the vault */
|
||||
registry: TimekeepRegistry | null = null;
|
||||
|
||||
constructor(app: ObsidianApp, manifest: PluginManifest) {
|
||||
super(app, manifest);
|
||||
|
||||
|
|
@ -90,6 +94,19 @@ export default class TimekeepPlugin extends Plugin {
|
|||
|
||||
this.addSettingTab(new TimekeepSettingsTab(this.app, this));
|
||||
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
const settings = this.settingsStore.getState();
|
||||
|
||||
if (!settings.registryEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize the registry (After the layout is ready and the vault is loaded)
|
||||
this.registry = new TimekeepRegistry(this.app.vault);
|
||||
this.registry.concurrencyLimit = settings.registryConcurrencyLimit;
|
||||
this.registry.onload();
|
||||
});
|
||||
|
||||
this.registerMarkdownCodeBlockProcessor(
|
||||
"timekeep",
|
||||
(source: string, el: HTMLElement, context: MarkdownPostProcessorContext) => {
|
||||
|
|
|
|||
194
src/service/registry.ts
Normal file
194
src/service/registry.ts
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import { EventRef, TAbstractFile, TFile, Vault } from "obsidian";
|
||||
import { limitFunction } from "p-limit";
|
||||
|
||||
import { createStore, Store } from "@/store";
|
||||
import {
|
||||
extractTimekeepCodeblocksWithPosition,
|
||||
type TimekeepWithPosition,
|
||||
} from "@/timekeep/parser";
|
||||
|
||||
type TimekeepRegistryEntry = {
|
||||
file: TFile;
|
||||
timekeeps: TimekeepWithPosition[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Obsidian vault registry of all timekeep instances
|
||||
*/
|
||||
export class TimekeepRegistry {
|
||||
/** Vault that the registry is operating on */
|
||||
vault: Vault;
|
||||
|
||||
/** Store for entries within the registry */
|
||||
entries: Store<TimekeepRegistryEntry[]>;
|
||||
|
||||
/** Tracked events */
|
||||
events: EventRef[] = [];
|
||||
|
||||
/** Concurrency limit for registry indexing */
|
||||
concurrencyLimit: number = 10;
|
||||
|
||||
constructor(vault: Vault) {
|
||||
this.vault = vault;
|
||||
this.entries = createStore([]);
|
||||
}
|
||||
|
||||
onload() {
|
||||
// Attach vault events
|
||||
const createEvent = this.vault.on("create", this.onFileCreated.bind(this));
|
||||
const modifyEvent = this.vault.on("modify", this.onFileModified.bind(this));
|
||||
const deleteEvent = this.vault.on("delete", this.onFileRemoved.bind(this));
|
||||
this.events.push(createEvent, modifyEvent, deleteEvent);
|
||||
|
||||
// Load the registry from the vault
|
||||
void this.loadFromVault()
|
||||
//
|
||||
.catch((error) => {
|
||||
console.error("failed to load vault timekeep data", error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the creation of new files with the vault, indexing them
|
||||
* into the timekeep registry
|
||||
*
|
||||
* @param file The file that was created
|
||||
*/
|
||||
onFileCreated(file: TAbstractFile) {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
void this.updateFromFile(file).catch(() =>
|
||||
console.error("failed to update timekeep changes within file")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a file being modified within the vault, reloads
|
||||
* the change and indexes it again
|
||||
*
|
||||
* @param file The modified file
|
||||
*/
|
||||
onFileModified(file: TAbstractFile) {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
void this.updateFromFile(file).catch(() =>
|
||||
console.error("failed to update timekeep changes within file")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a file being removed from the vault removes
|
||||
* the file if registered
|
||||
*
|
||||
* @param file The removed file
|
||||
*/
|
||||
onFileRemoved(file: TAbstractFile) {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.entries.setState((entries) => {
|
||||
const newEntries = entries.filter((entry) => {
|
||||
console.log(file, entry.file, file === entry.file);
|
||||
return entry.file !== file;
|
||||
});
|
||||
return newEntries;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the registry from the current vault
|
||||
*/
|
||||
async loadFromVault() {
|
||||
const entries = await TimekeepRegistry.getTimekeepsWithinVault(
|
||||
this.vault,
|
||||
true,
|
||||
this.concurrencyLimit
|
||||
);
|
||||
this.entries.setState(entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a file within the vault loading its timekeeps then
|
||||
* replacing them within the registry
|
||||
*
|
||||
* @param file
|
||||
*/
|
||||
async updateFromFile(file: TFile) {
|
||||
const timekeeps = await TimekeepRegistry.getTimekeepsWithinFile(this.vault, file, true);
|
||||
|
||||
this.entries.setState((entries) => {
|
||||
const filteredEntries: TimekeepRegistryEntry[] = entries.filter(
|
||||
(entry) => entry.file !== file
|
||||
);
|
||||
const newEntry: TimekeepRegistryEntry = {
|
||||
file,
|
||||
timekeeps,
|
||||
};
|
||||
|
||||
const newEntries = [...filteredEntries, newEntry];
|
||||
return newEntries;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all timekeep's within the provided vault
|
||||
*
|
||||
* @param vault The vault to search within
|
||||
* @param cached Whether to perform a cached read when checking
|
||||
* @param concurrencyLimit Maximum number of files to search at once
|
||||
* @returns The collection of timekeeps with their positions in each file
|
||||
*/
|
||||
static async getTimekeepsWithinVault(
|
||||
vault: Vault,
|
||||
cached: boolean = true,
|
||||
concurrencyLimit: number = 15
|
||||
): Promise<TimekeepRegistryEntry[]> {
|
||||
const markdownFiles = vault.getMarkdownFiles();
|
||||
|
||||
// Concurrency limited parallel file processing
|
||||
const processFile = limitFunction(
|
||||
async (file: TFile): Promise<TimekeepRegistryEntry> => {
|
||||
const timekeeps = await TimekeepRegistry.getTimekeepsWithinFile(
|
||||
vault,
|
||||
file,
|
||||
cached
|
||||
);
|
||||
|
||||
return { file, timekeeps };
|
||||
},
|
||||
{ concurrency: concurrencyLimit }
|
||||
);
|
||||
|
||||
const promises = markdownFiles.map(processFile);
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all timekeep's within the provided file in the
|
||||
* provided vault
|
||||
*
|
||||
* @param vault The vault to search within
|
||||
* @param file The file to search within
|
||||
* @param cached Whether to perform a cached read
|
||||
* @returns The collection of timekeeps with their positions in each file
|
||||
*/
|
||||
static async getTimekeepsWithinFile(
|
||||
vault: Vault,
|
||||
file: TFile,
|
||||
cached: boolean = true
|
||||
): Promise<TimekeepWithPosition[]> {
|
||||
let content: string;
|
||||
if (cached) {
|
||||
content = await vault.cachedRead(file);
|
||||
} else {
|
||||
content = await vault.read(file);
|
||||
}
|
||||
|
||||
return extractTimekeepCodeblocksWithPosition(content);
|
||||
}
|
||||
}
|
||||
|
|
@ -375,5 +375,49 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
}));
|
||||
});
|
||||
});
|
||||
|
||||
// Registry settings section
|
||||
new Setting(this.containerEl)
|
||||
.setName("Registry")
|
||||
.setDesc(
|
||||
"Timekeep uses an internal registry to track timekeep instances within your vault for functionality like autocomplete"
|
||||
)
|
||||
.setHeading();
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName("Enabled")
|
||||
.setDesc(
|
||||
"Whether to enable the registry, this can be disabled to reduce memory usage if you don't need the features that depend on it."
|
||||
)
|
||||
.addToggle((t) => {
|
||||
t.setValue(settings.registryEnabled);
|
||||
t.onChange((v) => {
|
||||
this.settingsStore.setState((currentValue) => ({
|
||||
...currentValue,
|
||||
registryEnabled: v,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName("Index Concurrency")
|
||||
.setDesc(
|
||||
"Maximum files to read concurrently on initialization (decrease this if you find you are lagging when opening your vault because of timekeep)"
|
||||
)
|
||||
|
||||
.addText((t) => {
|
||||
t.setValue(String(settings.registryConcurrencyLimit));
|
||||
t.onChange((v) => {
|
||||
const value = Number(v);
|
||||
|
||||
this.settingsStore.setState((currentValue) => ({
|
||||
...currentValue,
|
||||
registryConcurrencyLimit:
|
||||
Number.isFinite(value) && Number.isSafeInteger(value)
|
||||
? value
|
||||
: defaultSettings.registryConcurrencyLimit,
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,9 @@ export interface TimekeepSettings {
|
|||
|
||||
sortOrder: SortOrder;
|
||||
unstartedOrder: UnstartedOrder;
|
||||
|
||||
registryEnabled: boolean;
|
||||
registryConcurrencyLimit: number;
|
||||
}
|
||||
|
||||
export const defaultSettings: TimekeepSettings = {
|
||||
|
|
@ -92,6 +95,9 @@ export const defaultSettings: TimekeepSettings = {
|
|||
|
||||
sortOrder: SortOrder.INSERTION,
|
||||
unstartedOrder: UnstartedOrder.LAST,
|
||||
|
||||
registryEnabled: true,
|
||||
registryConcurrencyLimit: 15,
|
||||
};
|
||||
|
||||
export function legacySettingsCompatibility(settings: TimekeepSettings): void {
|
||||
|
|
|
|||
Loading…
Reference in a new issue