mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(filesource): enhance FileSource task handling and WriteAPI support
- Add FileSource task update capability in WriteAPI - Improve file type detection and metadata mapping - Add comprehensive FileSource tests for basic operations - Update Repository to handle FileSource task operations - Enhance FileSource with filter manager integration
This commit is contained in:
parent
c7db2b5c65
commit
4c5f560c2b
6 changed files with 387 additions and 227 deletions
|
|
@ -66,17 +66,14 @@ describe('FileSource', () => {
|
|||
fileTaskProperties: {
|
||||
contentSource: 'filename',
|
||||
stripExtension: true,
|
||||
defaultStatus: ' '
|
||||
defaultStatus: ' ',
|
||||
preferFrontmatterTitle: true
|
||||
},
|
||||
performance: {
|
||||
enableWorkerProcessing: false,
|
||||
enableCaching: true,
|
||||
cacheTTL: 300000
|
||||
},
|
||||
advanced: {
|
||||
excludePatterns: ['**/.obsidian/**'],
|
||||
maxFileSize: 1048576
|
||||
}
|
||||
};
|
||||
|
||||
fileSource = new FileSource(mockApp, config);
|
||||
|
|
|
|||
|
|
@ -74,7 +74,8 @@ describe('FileSourceConfig', () => {
|
|||
fileTaskProperties: {
|
||||
contentSource: 'title',
|
||||
stripExtension: false,
|
||||
defaultStatus: 'x'
|
||||
defaultStatus: 'x',
|
||||
preferFrontmatterTitle: true
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -241,7 +242,8 @@ describe('FileSourceConfig', () => {
|
|||
fileTaskProperties: {
|
||||
contentSource: 'custom',
|
||||
stripExtension: true,
|
||||
defaultStatus: ' '
|
||||
defaultStatus: ' ',
|
||||
preferFrontmatterTitle: true
|
||||
// Missing customContentField
|
||||
}
|
||||
};
|
||||
|
|
@ -262,31 +264,6 @@ describe('FileSourceConfig', () => {
|
|||
const errors = config.validateConfig(invalidConfig);
|
||||
expect(errors).toContain('Cache TTL must be a positive number');
|
||||
});
|
||||
|
||||
it('should validate maximum file size', () => {
|
||||
const invalidConfig: Partial<FileSourceConfiguration> = {
|
||||
advanced: {
|
||||
excludePatterns: [],
|
||||
maxFileSize: 100 // Too small
|
||||
}
|
||||
};
|
||||
|
||||
const errors = config.validateConfig(invalidConfig);
|
||||
expect(errors).toContain('Maximum file size must be at least 1KB');
|
||||
});
|
||||
|
||||
it('should validate custom recognition function syntax', () => {
|
||||
const invalidConfig: Partial<FileSourceConfiguration> = {
|
||||
advanced: {
|
||||
excludePatterns: [],
|
||||
maxFileSize: 1024,
|
||||
customRecognitionFunction: 'invalid javascript syntax {'
|
||||
}
|
||||
};
|
||||
|
||||
const errors = config.validateConfig(invalidConfig);
|
||||
expect(errors).toContain('Custom recognition function has invalid syntax');
|
||||
});
|
||||
});
|
||||
|
||||
describe('configuration presets', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* WriteAPI - Handles all write operations in the Dataflow architecture
|
||||
*
|
||||
*
|
||||
* This API provides methods for creating, updating, and deleting tasks
|
||||
* by directly modifying vault files. Changes trigger ObsidianSource events
|
||||
* which automatically update the index through the Orchestrator.
|
||||
|
|
@ -75,7 +75,7 @@ export interface BatchCreateSubtasksArgs {
|
|||
|
||||
export class WriteAPI {
|
||||
private canvasTaskUpdater: CanvasTaskUpdater;
|
||||
|
||||
|
||||
constructor(
|
||||
private app: App,
|
||||
private vault: Vault,
|
||||
|
|
@ -150,7 +150,7 @@ export class WriteAPI {
|
|||
}
|
||||
|
||||
lines[task.line] = taskLine;
|
||||
|
||||
|
||||
// Notify about write operation
|
||||
emit(this.app, Events.WRITE_OPERATION_START, { path: file.path, taskId: args.taskId });
|
||||
await this.vault.modify(file, lines.join("\n"));
|
||||
|
|
@ -184,6 +184,12 @@ export class WriteAPI {
|
|||
return this.updateCanvasTask(args);
|
||||
}
|
||||
|
||||
// Handle FileSource (file-level) tasks differently
|
||||
const isFileSourceTask = (originalTask as any)?.metadata?.source === "file-source" || originalTask.id.startsWith("file-source:");
|
||||
if (isFileSourceTask) {
|
||||
return this.updateFileSourceTask(originalTask, args.updates, args.taskId);
|
||||
}
|
||||
|
||||
const file = this.vault.getAbstractFileByPath(originalTask.filePath) as TFile;
|
||||
if (!file) {
|
||||
return { success: false, error: "File not found" };
|
||||
|
|
@ -236,26 +242,26 @@ export class WriteAPI {
|
|||
}
|
||||
|
||||
lines[originalTask.line] = taskLine;
|
||||
|
||||
|
||||
// Notify about write operation
|
||||
emit(this.app, Events.WRITE_OPERATION_START, { path: file.path, taskId: args.taskId });
|
||||
await this.vault.modify(file, lines.join("\n"));
|
||||
|
||||
|
||||
// Create the updated task object with the new content
|
||||
const updatedTaskObj: Task = {
|
||||
...originalTask,
|
||||
...args.updates,
|
||||
originalMarkdown: taskLine.replace(/^\s*[-*+]\s*\[[^\]]*\]\s*/, ""), // Remove checkbox prefix
|
||||
};
|
||||
|
||||
|
||||
// Emit task updated event for direct update in dataflow
|
||||
emit(this.app, Events.TASK_UPDATED, { task: updatedTaskObj });
|
||||
|
||||
|
||||
// Trigger task-completed event if task was just completed
|
||||
if (args.updates.completed === true && !originalTask.completed) {
|
||||
this.app.workspace.trigger("task-genius:task-completed", updatedTaskObj);
|
||||
}
|
||||
|
||||
|
||||
// Still emit write operation complete for compatibility
|
||||
emit(this.app, Events.WRITE_OPERATION_COMPLETE, { path: file.path, taskId: args.taskId });
|
||||
|
||||
|
|
@ -266,6 +272,132 @@ export class WriteAPI {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update a FileSource (file-level) task. This updates frontmatter title, H1, or filename
|
||||
* depending on settings, instead of trying to edit a markdown checkbox line.
|
||||
*/
|
||||
private async updateFileSourceTask(
|
||||
originalTask: Task,
|
||||
updates: Partial<Task>,
|
||||
taskId: string
|
||||
): Promise<{ success: boolean; task?: Task; error?: string }> {
|
||||
const file = this.vault.getAbstractFileByPath(originalTask.filePath) as TFile;
|
||||
if (!file) {
|
||||
return { success: false, error: "File not found" };
|
||||
}
|
||||
|
||||
let newFilePath = originalTask.filePath;
|
||||
const cfg = this.plugin.settings?.fileSource?.fileTaskProperties;
|
||||
const contentSource: "filename" | "title" | "h1" | "custom" = cfg?.contentSource ?? "filename";
|
||||
const preferFrontmatterTitle = cfg?.preferFrontmatterTitle ?? true;
|
||||
const customContentField = (this.plugin.settings?.fileSource?.fileTaskProperties as any)?.customContentField as string | undefined;
|
||||
|
||||
// Handle content/title change
|
||||
if (updates.content && updates.content !== originalTask.content) {
|
||||
try {
|
||||
// Announce start of a write operation
|
||||
emit(this.app, Events.WRITE_OPERATION_START, { path: file.path, taskId });
|
||||
|
||||
switch (contentSource) {
|
||||
case "title": {
|
||||
if (preferFrontmatterTitle) {
|
||||
await this.app.fileManager.processFrontMatter(file, (fm) => {
|
||||
(fm as any).title = updates.content;
|
||||
});
|
||||
} else {
|
||||
newFilePath = await this.renameFile(file, updates.content!);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "h1": {
|
||||
await this.updateH1Heading(file, updates.content!);
|
||||
break;
|
||||
}
|
||||
case "custom": {
|
||||
if (customContentField) {
|
||||
await this.app.fileManager.processFrontMatter(file, (fm) => {
|
||||
(fm as any)[customContentField] = updates.content;
|
||||
});
|
||||
} else {
|
||||
newFilePath = await this.renameFile(file, updates.content!);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "filename":
|
||||
default: {
|
||||
newFilePath = await this.renameFile(file, updates.content!);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Announce completion of write operation
|
||||
emit(this.app, Events.WRITE_OPERATION_COMPLETE, { path: newFilePath, taskId });
|
||||
} catch (error) {
|
||||
console.error("WriteAPI: Error updating file-source task content:", error);
|
||||
return { success: false, error: String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
// Build the updated task object
|
||||
const updatedTaskObj: Task = {
|
||||
...originalTask,
|
||||
...updates,
|
||||
filePath: newFilePath,
|
||||
// Keep id in sync with FileSource convention when path changes
|
||||
id: (originalTask.id.startsWith("file-source:") && newFilePath !== originalTask.filePath)
|
||||
? `file-source:${newFilePath}`
|
||||
: originalTask.id,
|
||||
originalMarkdown: `[${updates.content ?? originalTask.content}](${newFilePath})`,
|
||||
};
|
||||
|
||||
// Emit file-task update so repository updates fileTasks map directly
|
||||
emit(this.app, Events.FILE_TASK_UPDATED, { task: updatedTaskObj });
|
||||
|
||||
return { success: true, task: updatedTaskObj };
|
||||
}
|
||||
|
||||
private async updateH1Heading(file: TFile, newHeading: string): Promise<void> {
|
||||
const content = await this.vault.read(file);
|
||||
const lines = content.split("\n");
|
||||
// Find first H1 after optional frontmatter
|
||||
let h1Index = -1;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].startsWith("# ")) { h1Index = i; break; }
|
||||
}
|
||||
if (h1Index >= 0) {
|
||||
lines[h1Index] = `# ${newHeading}`;
|
||||
} else {
|
||||
let insertIndex = 0;
|
||||
if (content.startsWith("---")) {
|
||||
const fmEnd = content.indexOf("\n---\n", 3);
|
||||
if (fmEnd >= 0) {
|
||||
const fmLines = content.substring(0, fmEnd + 5).split("\n").length - 1;
|
||||
insertIndex = fmLines;
|
||||
}
|
||||
}
|
||||
lines.splice(insertIndex, 0, `# ${newHeading}`, "");
|
||||
}
|
||||
await this.vault.modify(file, lines.join("\n"));
|
||||
}
|
||||
|
||||
private async renameFile(file: TFile, newTitle: string): Promise<string> {
|
||||
const currentPath = file.path;
|
||||
const lastSlash = currentPath.lastIndexOf("/");
|
||||
const directory = lastSlash > 0 ? currentPath.substring(0, lastSlash) : "";
|
||||
const extension = currentPath.substring(currentPath.lastIndexOf("."));
|
||||
const sanitized = this.sanitizeFileName(newTitle);
|
||||
const newPath = directory ? `${directory}/${sanitized}${extension}` : `${sanitized}${extension}`;
|
||||
if (newPath !== currentPath) {
|
||||
await this.vault.rename(file, newPath);
|
||||
}
|
||||
return newPath;
|
||||
}
|
||||
|
||||
private sanitizeFileName(name: string): string {
|
||||
return name.replace(/[<>:"/\\|?*]/g, "_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new task
|
||||
*/
|
||||
|
|
@ -320,7 +452,7 @@ export class WriteAPI {
|
|||
const newContent = args.parent
|
||||
? this.insertSubtask(content, args.parent, taskContent)
|
||||
: (content ? content + "\n" : "") + taskContent;
|
||||
|
||||
|
||||
// Notify about write operation
|
||||
emit(this.app, Events.WRITE_OPERATION_START, { path: file.path });
|
||||
await this.vault.modify(file, newContent);
|
||||
|
|
@ -359,7 +491,7 @@ export class WriteAPI {
|
|||
|
||||
if (task.line >= 0 && task.line < lines.length) {
|
||||
lines.splice(task.line, 1);
|
||||
|
||||
|
||||
// Notify about write operation
|
||||
emit(this.app, Events.WRITE_OPERATION_START, { path: file.path, taskId: args.taskId });
|
||||
await this.vault.modify(file, lines.join("\n"));
|
||||
|
|
@ -500,7 +632,7 @@ export class WriteAPI {
|
|||
try {
|
||||
// Try using Daily Notes plugin if available
|
||||
let dailyNoteFile: TFile | null = null;
|
||||
|
||||
|
||||
if (appHasDailyNotesPluginLoaded()) {
|
||||
const date = moment().set("hour", 12);
|
||||
const existing = getDailyNote(date, getAllDailyNotes());
|
||||
|
|
@ -685,7 +817,7 @@ export class WriteAPI {
|
|||
metadata.push(`[tags:: ${args.tags.join(", ")}]`);
|
||||
} else {
|
||||
// Ensure tags don't already have # prefix before adding one
|
||||
metadata.push(...args.tags.map(tag =>
|
||||
metadata.push(...args.tags.map(tag =>
|
||||
tag.startsWith("#") ? tag : `#${tag}`
|
||||
));
|
||||
}
|
||||
|
|
@ -884,12 +1016,12 @@ export class WriteAPI {
|
|||
if (result.success) {
|
||||
// Emit task updated event for dataflow
|
||||
emit(this.app, Events.TASK_UPDATED, { task: updatedTask });
|
||||
|
||||
|
||||
// Trigger task-completed event if task was just completed
|
||||
if (args.updates.completed === true && !originalTask.completed) {
|
||||
this.app.workspace.trigger("task-genius:task-completed", updatedTask);
|
||||
}
|
||||
|
||||
|
||||
return { success: true, task: updatedTask };
|
||||
} else {
|
||||
return { success: false, error: result.error };
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export class Repository {
|
|||
private sourceSeq: number = 0; // Track source sequence to differentiate events
|
||||
private icsEvents: Task[] = []; // Store ICS events separately
|
||||
private fileTasks = new Map<string, Task>(); // Store file-level tasks
|
||||
|
||||
|
||||
// Persistence queue management
|
||||
private persistQueue = new Set<string>();
|
||||
private persistTimer: NodeJS.Timeout | null = null;
|
||||
|
|
@ -34,27 +34,34 @@ export class Repository {
|
|||
this.storage = new Storage(app.appId || "obsidian-task-genius", "1.0.0");
|
||||
}
|
||||
|
||||
/** Allow orchestrator to pass a central FileFilterManager down to indexer */
|
||||
public setFileFilterManager(filterManager: any) {
|
||||
// TaskIndexer has setFileFilterManager API
|
||||
(this.indexer as any).setFileFilterManager?.(filterManager);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the repository (load persisted data if available)
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
console.log("[Repository] Initializing repository...");
|
||||
|
||||
|
||||
try {
|
||||
// Try to load consolidated index from storage
|
||||
console.log("[Repository] Attempting to load consolidated index from storage...");
|
||||
const consolidated = await this.storage.loadConsolidated();
|
||||
|
||||
|
||||
if (consolidated && consolidated.data) {
|
||||
// Restore the index from persisted data
|
||||
const snapshotTaskCount = consolidated.data?.tasks ?
|
||||
const snapshotTaskCount = consolidated.data?.tasks ?
|
||||
(consolidated.data.tasks instanceof Map ? consolidated.data.tasks.size : Object.keys(consolidated.data.tasks).length) : 0;
|
||||
console.log(`[Repository] Found persisted snapshot with ${snapshotTaskCount} tasks, restoring...`);
|
||||
await this.indexer.restoreFromSnapshot(consolidated.data);
|
||||
|
||||
|
||||
const taskCount = await this.indexer.getTotalTaskCount();
|
||||
console.log(`[Repository] Index restored successfully with ${taskCount} tasks`);
|
||||
|
||||
|
||||
// Emit cache ready event
|
||||
emit(this.app, Events.CACHE_READY, {
|
||||
initial: true,
|
||||
|
|
@ -64,7 +71,7 @@ export class Repository {
|
|||
} else {
|
||||
console.log("[Repository] No persisted data found, starting with empty index");
|
||||
}
|
||||
|
||||
|
||||
// Load ICS events from storage
|
||||
console.log("[Repository] Loading ICS events from storage...");
|
||||
this.icsEvents = await this.storage.loadIcsEvents();
|
||||
|
|
@ -85,20 +92,20 @@ export class Repository {
|
|||
async updateFile(filePath: string, tasks: Task[], sourceSeq?: number): Promise<void> {
|
||||
// Check if tasks have actually changed
|
||||
const existingAugmented = await this.storage.loadAugmented(filePath);
|
||||
const hasChanges = !existingAugmented ||
|
||||
const hasChanges = !existingAugmented ||
|
||||
JSON.stringify(tasks) !== JSON.stringify(existingAugmented.data);
|
||||
|
||||
|
||||
// Always update the in-memory index for consistency
|
||||
await this.indexer.updateIndexWithTasks(filePath, tasks);
|
||||
|
||||
|
||||
// Always store augmented tasks to cache
|
||||
await this.storage.storeAugmented(filePath, tasks);
|
||||
|
||||
|
||||
// Schedule persist operation for single file updates
|
||||
if (hasChanges) {
|
||||
this.schedulePersist(filePath);
|
||||
}
|
||||
|
||||
|
||||
// Only emit update event if there are actual changes
|
||||
if (hasChanges) {
|
||||
this.lastSequence = Seq.next();
|
||||
|
|
@ -129,12 +136,12 @@ export class Repository {
|
|||
for (const [filePath, tasks] of updates) {
|
||||
// Check if tasks have actually changed
|
||||
const existingAugmented = await this.storage.loadAugmented(filePath);
|
||||
const hasChanges = !existingAugmented ||
|
||||
const hasChanges = !existingAugmented ||
|
||||
JSON.stringify(tasks) !== JSON.stringify(existingAugmented.data);
|
||||
|
||||
|
||||
await this.indexer.updateIndexWithTasks(filePath, tasks);
|
||||
await this.storage.storeAugmented(filePath, tasks);
|
||||
|
||||
|
||||
if (hasChanges) {
|
||||
changedFiles.push(filePath);
|
||||
totalChanged += tasks.length;
|
||||
|
|
@ -170,10 +177,10 @@ export class Repository {
|
|||
*/
|
||||
async removeFile(filePath: string): Promise<void> {
|
||||
await this.indexer.removeTasksFromFile(filePath);
|
||||
|
||||
|
||||
// Clear storage for this file
|
||||
await this.storage.clearFile(filePath);
|
||||
|
||||
|
||||
// Emit update event
|
||||
this.lastSequence = Seq.next();
|
||||
emit(this.app, Events.TASK_CACHE_UPDATED, {
|
||||
|
|
@ -192,13 +199,13 @@ export class Repository {
|
|||
*/
|
||||
async updateIcsEvents(events: Task[], sourceSeq?: number): Promise<void> {
|
||||
console.log(`[Repository] Updating ${events.length} ICS events`);
|
||||
|
||||
|
||||
// Store the new ICS events
|
||||
this.icsEvents = events;
|
||||
|
||||
|
||||
// Store ICS events to persistence
|
||||
await this.storage.storeIcsEvents(events);
|
||||
|
||||
|
||||
// Emit update event to notify views
|
||||
this.lastSequence = Seq.next();
|
||||
emit(this.app, Events.TASK_CACHE_UPDATED, {
|
||||
|
|
@ -238,12 +245,12 @@ export class Repository {
|
|||
async byProject(project: string): Promise<Task[]> {
|
||||
const taskIds = await this.indexer.getTaskIdsByProject(project);
|
||||
const fileTasks = await this.getTasksByIds(taskIds);
|
||||
|
||||
|
||||
// Also filter ICS events by project if they have one
|
||||
const icsProjectTasks = this.icsEvents.filter(task =>
|
||||
const icsProjectTasks = this.icsEvents.filter(task =>
|
||||
task.metadata?.project === project
|
||||
);
|
||||
|
||||
|
||||
return [...fileTasks, ...icsProjectTasks];
|
||||
}
|
||||
|
||||
|
|
@ -254,15 +261,15 @@ export class Repository {
|
|||
const taskIdSets = await Promise.all(
|
||||
tags.map(tag => this.indexer.getTaskIdsByTag(tag))
|
||||
);
|
||||
|
||||
|
||||
// Find intersection of all tag sets
|
||||
if (taskIdSets.length === 0) return [];
|
||||
|
||||
|
||||
let intersection = new Set(taskIdSets[0]);
|
||||
for (let i = 1; i < taskIdSets.length; i++) {
|
||||
intersection = new Set([...intersection].filter(id => taskIdSets[i].has(id)));
|
||||
}
|
||||
|
||||
|
||||
return this.getTasksByIds(intersection);
|
||||
}
|
||||
|
||||
|
|
@ -277,31 +284,31 @@ export class Repository {
|
|||
/**
|
||||
* Get tasks by date range
|
||||
*/
|
||||
async byDateRange(opts: {
|
||||
from?: number;
|
||||
to?: number;
|
||||
field?: "due" | "start" | "scheduled"
|
||||
async byDateRange(opts: {
|
||||
from?: number;
|
||||
to?: number;
|
||||
field?: "due" | "start" | "scheduled"
|
||||
}): Promise<Task[]> {
|
||||
const field = opts.field || "due";
|
||||
const cache = await this.indexer.getCache();
|
||||
|
||||
|
||||
const dateIndex = field === "due" ? cache.dueDate :
|
||||
field === "start" ? cache.startDate :
|
||||
cache.scheduledDate;
|
||||
|
||||
|
||||
const taskIds = new Set<string>();
|
||||
|
||||
|
||||
for (const [dateStr, ids] of dateIndex) {
|
||||
const date = new Date(dateStr).getTime();
|
||||
|
||||
|
||||
if (opts.from && date < opts.from) continue;
|
||||
if (opts.to && date > opts.to) continue;
|
||||
|
||||
|
||||
for (const id of ids) {
|
||||
taskIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return this.getTasksByIds(taskIds);
|
||||
}
|
||||
|
||||
|
|
@ -330,22 +337,22 @@ export class Repository {
|
|||
byStatus: Map<boolean, number>;
|
||||
}> {
|
||||
const cache = await this.indexer.getCache();
|
||||
|
||||
|
||||
const byProject = new Map<string, number>();
|
||||
for (const [project, ids] of cache.projects) {
|
||||
byProject.set(project, ids.size);
|
||||
}
|
||||
|
||||
|
||||
const byTag = new Map<string, number>();
|
||||
for (const [tag, ids] of cache.tags) {
|
||||
byTag.set(tag, ids.size);
|
||||
}
|
||||
|
||||
|
||||
const byStatus = new Map<boolean, number>();
|
||||
for (const [status, ids] of cache.completed) {
|
||||
byStatus.set(status, ids.size);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
total: cache.tasks.size,
|
||||
byProject,
|
||||
|
|
@ -367,12 +374,12 @@ export class Repository {
|
|||
*/
|
||||
private schedulePersist(source: string): void {
|
||||
this.persistQueue.add(source);
|
||||
|
||||
|
||||
// Check if we should persist immediately
|
||||
const shouldPersistNow =
|
||||
const shouldPersistNow =
|
||||
this.persistQueue.size >= this.MAX_QUEUE_SIZE ||
|
||||
(Date.now() - this.lastPersistTime) > this.MAX_PERSIST_INTERVAL;
|
||||
|
||||
|
||||
if (shouldPersistNow) {
|
||||
this.executePersist();
|
||||
} else {
|
||||
|
|
@ -394,7 +401,7 @@ export class Repository {
|
|||
clearTimeout(this.persistTimer);
|
||||
this.persistTimer = null;
|
||||
}
|
||||
|
||||
|
||||
if (this.persistQueue.size > 0) {
|
||||
const queueSize = this.persistQueue.size;
|
||||
console.log(`[Repository] Persisting after ${queueSize} changes`);
|
||||
|
|
@ -439,14 +446,14 @@ export class Repository {
|
|||
private async getTasksByIds(taskIds: Set<string> | string[]): Promise<Task[]> {
|
||||
const tasks: Task[] = [];
|
||||
const ids = Array.isArray(taskIds) ? taskIds : Array.from(taskIds);
|
||||
|
||||
|
||||
for (const id of ids) {
|
||||
const task = await this.indexer.getTaskById(id);
|
||||
if (task) {
|
||||
tasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
|
|
@ -464,13 +471,13 @@ export class Repository {
|
|||
async updateFileTask(task: Task): Promise<void> {
|
||||
const filePath = task.filePath;
|
||||
if (!filePath) return;
|
||||
|
||||
|
||||
// Store the file task
|
||||
this.fileTasks.set(filePath, task);
|
||||
|
||||
|
||||
// Schedule persist for file tasks
|
||||
this.schedulePersist(`file-task:${filePath}`);
|
||||
|
||||
|
||||
// Emit update event
|
||||
this.lastSequence = Seq.next();
|
||||
emit(this.app, Events.TASK_CACHE_UPDATED, {
|
||||
|
|
@ -491,10 +498,10 @@ export class Repository {
|
|||
async getTaskById(taskId: string): Promise<Task | undefined> {
|
||||
// Get all tasks from the repository
|
||||
const allTasks = await this.all();
|
||||
|
||||
|
||||
// Find the task by ID
|
||||
const task = allTasks.find(t => t.id === taskId);
|
||||
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
|
|
@ -505,35 +512,35 @@ export class Repository {
|
|||
async updateSingleTask(updatedTask: Task): Promise<void> {
|
||||
const filePath = updatedTask.filePath;
|
||||
if (!filePath) return;
|
||||
|
||||
|
||||
console.log(`[Repository] Updating single task: ${updatedTask.id} in ${filePath}`);
|
||||
|
||||
|
||||
// Load existing augmented tasks for the file
|
||||
const existingAugmented = await this.storage.loadAugmented(filePath);
|
||||
if (!existingAugmented) {
|
||||
console.warn(`[Repository] No existing tasks found for ${filePath}, cannot update single task`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Find and replace the task in the array
|
||||
const tasks = existingAugmented.data;
|
||||
const taskIndex = tasks.findIndex(t => t.id === updatedTask.id);
|
||||
|
||||
|
||||
if (taskIndex === -1) {
|
||||
console.warn(`[Repository] Task ${updatedTask.id} not found in ${filePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Update the task
|
||||
tasks[taskIndex] = updatedTask;
|
||||
|
||||
|
||||
// Update the index and storage
|
||||
await this.indexer.updateIndexWithTasks(filePath, tasks);
|
||||
await this.storage.storeAugmented(filePath, tasks);
|
||||
|
||||
|
||||
// Schedule persist operation
|
||||
this.schedulePersist(filePath);
|
||||
|
||||
|
||||
// Emit update event
|
||||
this.lastSequence = Seq.next();
|
||||
emit(this.app, Events.TASK_CACHE_UPDATED, {
|
||||
|
|
@ -546,7 +553,7 @@ export class Repository {
|
|||
seq: this.lastSequence,
|
||||
sourceSeq: undefined
|
||||
});
|
||||
|
||||
|
||||
console.log(`[Repository] Single task ${updatedTask.id} updated successfully`);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
/**
|
||||
* FileSource - Main implementation for FileSource feature
|
||||
*
|
||||
*
|
||||
* This source integrates files as tasks into the dataflow architecture.
|
||||
* It follows the same patterns as ObsidianSource and IcsSource.
|
||||
*/
|
||||
|
||||
import type { App, TFile, EventRef, CachedMetadata } from "obsidian";
|
||||
import type { Task } from "../../types/task";
|
||||
import type {
|
||||
FileSourceConfiguration,
|
||||
FileSourceTaskMetadata,
|
||||
FileSourceStats,
|
||||
import type {
|
||||
FileSourceConfiguration,
|
||||
FileSourceTaskMetadata,
|
||||
FileSourceStats,
|
||||
UpdateDecision,
|
||||
FileTaskCache,
|
||||
RecognitionStrategy,
|
||||
|
|
@ -20,9 +20,11 @@ import type {
|
|||
import { Events, emit, Seq, on } from "../events/Events";
|
||||
import { FileSourceConfig } from "./FileSourceConfig";
|
||||
|
||||
import { FileFilterManager } from "../../managers/file-filter-manager";
|
||||
|
||||
/**
|
||||
* FileSource - Independent event source for file-based tasks
|
||||
*
|
||||
*
|
||||
* Subscribes to file events and transforms qualifying files into tasks
|
||||
* following the established dataflow patterns.
|
||||
*/
|
||||
|
|
@ -30,17 +32,17 @@ export class FileSource {
|
|||
private config: FileSourceConfig;
|
||||
private isInitialized = false;
|
||||
private lastUpdateSeq = 0;
|
||||
|
||||
|
||||
// Event references for cleanup
|
||||
private eventRefs: EventRef[] = [];
|
||||
|
||||
|
||||
// Cache for tracking file task state
|
||||
private fileTaskCache = new Map<string, FileTaskCache>();
|
||||
|
||||
|
||||
// Debouncing for rapid changes
|
||||
private pendingUpdates = new Map<string, NodeJS.Timeout>();
|
||||
private readonly DEBOUNCE_DELAY = 300; // ms
|
||||
|
||||
|
||||
// Statistics tracking
|
||||
private stats: FileSourceStats = {
|
||||
initialized: false,
|
||||
|
|
@ -57,7 +59,8 @@ export class FileSource {
|
|||
|
||||
constructor(
|
||||
private app: App,
|
||||
initialConfig?: Partial<FileSourceConfiguration>
|
||||
initialConfig?: Partial<FileSourceConfiguration>,
|
||||
private fileFilterManager?: FileFilterManager
|
||||
) {
|
||||
this.config = new FileSourceConfig(initialConfig);
|
||||
}
|
||||
|
|
@ -70,21 +73,23 @@ export class FileSource {
|
|||
if (!this.config.isEnabled()) return;
|
||||
|
||||
console.log("[FileSource] Initializing FileSource...");
|
||||
|
||||
|
||||
// Subscribe to configuration changes
|
||||
this.config.onChange((newConfig) => {
|
||||
this.handleConfigChange(newConfig);
|
||||
});
|
||||
|
||||
|
||||
// Subscribe to file events
|
||||
this.subscribeToFileEvents();
|
||||
|
||||
// Perform initial scan of existing files
|
||||
this.performInitialScan();
|
||||
|
||||
|
||||
// Delay initial scan to ensure vault is fully loaded
|
||||
setTimeout(() => {
|
||||
this.performInitialScan();
|
||||
}, 1000); // 1 second delay
|
||||
|
||||
this.isInitialized = true;
|
||||
this.stats.initialized = true;
|
||||
|
||||
|
||||
console.log(`[FileSource] Initialized with strategies: ${this.config.getEnabledStrategies().join(', ')}`);
|
||||
}
|
||||
|
||||
|
|
@ -136,7 +141,7 @@ export class FileSource {
|
|||
// Set new debounced timeout
|
||||
const timeout = setTimeout(async () => {
|
||||
this.pendingUpdates.delete(filePath);
|
||||
|
||||
|
||||
try {
|
||||
await this.processFileUpdate(filePath, reason);
|
||||
} catch (error) {
|
||||
|
|
@ -172,6 +177,10 @@ export class FileSource {
|
|||
return;
|
||||
}
|
||||
|
||||
// Apply centralized file filter manager for file-scope rules
|
||||
if (this.fileFilterManager && !this.fileFilterManager.shouldIncludePath(filePath, "file")) {
|
||||
return;
|
||||
}
|
||||
const shouldBeTask = await this.shouldCreateFileTask(filePath);
|
||||
const existingCache = this.fileTaskCache.get(filePath);
|
||||
const wasTask = existingCache?.fileTaskExists ?? false;
|
||||
|
|
@ -201,7 +210,7 @@ export class FileSource {
|
|||
try {
|
||||
const fileContent = await this.app.vault.cachedRead(file);
|
||||
const fileCache = this.app.metadataCache.getFileCache(file);
|
||||
|
||||
|
||||
return this.evaluateRecognitionStrategies(filePath, fileContent, fileCache);
|
||||
} catch (error) {
|
||||
console.error(`[FileSource] Error reading file ${filePath}:`, error);
|
||||
|
|
@ -213,8 +222,8 @@ export class FileSource {
|
|||
* Evaluate all enabled recognition strategies
|
||||
*/
|
||||
private evaluateRecognitionStrategies(
|
||||
filePath: string,
|
||||
fileContent: string,
|
||||
filePath: string,
|
||||
fileContent: string,
|
||||
fileCache: CachedMetadata | null
|
||||
): boolean {
|
||||
const config = this.config.getConfig();
|
||||
|
|
@ -265,7 +274,7 @@ export class FileSource {
|
|||
const { taskFields, requireAllFields } = config;
|
||||
const frontmatter = fileCache.frontmatter;
|
||||
|
||||
const matchingFields = taskFields.filter((field: string) =>
|
||||
const matchingFields = taskFields.filter((field: string) =>
|
||||
frontmatter.hasOwnProperty(field) && frontmatter[field] !== undefined
|
||||
);
|
||||
|
||||
|
|
@ -331,14 +340,14 @@ export class FileSource {
|
|||
if (!config.enabled || !config.taskPaths || config.taskPaths.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Normalize path (use forward slashes)
|
||||
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||
|
||||
|
||||
// Check each configured path pattern
|
||||
for (const pattern of config.taskPaths) {
|
||||
const normalizedPattern = pattern.replace(/\\/g, '/');
|
||||
|
||||
|
||||
switch (config.matchMode) {
|
||||
case 'prefix':
|
||||
if (normalizedPath.startsWith(normalizedPattern)) {
|
||||
|
|
@ -346,7 +355,7 @@ export class FileSource {
|
|||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'regex':
|
||||
try {
|
||||
const regex = new RegExp(normalizedPattern);
|
||||
|
|
@ -358,7 +367,7 @@ export class FileSource {
|
|||
console.warn(`[FileSource] Invalid regex pattern: ${pattern}`, e);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'glob':
|
||||
if (this.matchGlobPattern(normalizedPath, normalizedPattern)) {
|
||||
console.log(`[FileSource] Path matches glob pattern: ${pattern} for ${filePath}`);
|
||||
|
|
@ -367,7 +376,7 @@ export class FileSource {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -383,14 +392,14 @@ export class FileSource {
|
|||
.replace(/\*/g, '[^/]*') // * matches any chars except /
|
||||
.replace(/§§§/g, '.*') // ** matches any chars
|
||||
.replace(/\?/g, '[^/]'); // ? matches single char
|
||||
|
||||
|
||||
// If pattern ends with /, match all files in that directory
|
||||
if (pattern.endsWith('/')) {
|
||||
regexPattern = `^${regexPattern}.*`;
|
||||
} else {
|
||||
regexPattern = `^${regexPattern}$`;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const regex = new RegExp(regexPattern);
|
||||
return regex.test(path);
|
||||
|
|
@ -410,19 +419,19 @@ export class FileSource {
|
|||
try {
|
||||
const fileContent = await this.app.vault.cachedRead(file);
|
||||
const fileCache = this.app.metadataCache.getFileCache(file);
|
||||
|
||||
|
||||
const fileTask = await this.buildFileTask(filePath, fileContent, fileCache);
|
||||
if (!fileTask) return null;
|
||||
|
||||
// Update cache
|
||||
this.updateFileTaskCache(filePath, fileTask);
|
||||
|
||||
|
||||
// Update statistics
|
||||
this.updateStatistics(fileTask.metadata.recognitionStrategy, 1);
|
||||
|
||||
|
||||
// Emit file task event
|
||||
this.emitFileTaskUpdate('created', fileTask);
|
||||
|
||||
|
||||
return fileTask;
|
||||
} catch (error) {
|
||||
console.error(`[FileSource] Error creating file task for ${filePath}:`, error);
|
||||
|
|
@ -448,20 +457,20 @@ export class FileSource {
|
|||
|
||||
// Remove from cache
|
||||
this.fileTaskCache.delete(filePath);
|
||||
|
||||
|
||||
// Update statistics
|
||||
this.stats.trackedFileCount = Math.max(0, this.stats.trackedFileCount - 1);
|
||||
|
||||
|
||||
// Emit removal event
|
||||
const seq = Seq.next();
|
||||
this.lastUpdateSeq = seq;
|
||||
|
||||
|
||||
emit(this.app, Events.FILE_TASK_REMOVED, {
|
||||
filePath,
|
||||
timestamp: Date.now(),
|
||||
seq
|
||||
});
|
||||
|
||||
|
||||
console.log(`[FileSource] Removed file task: ${filePath}`);
|
||||
}
|
||||
|
||||
|
|
@ -524,25 +533,25 @@ export class FileSource {
|
|||
fileCache: CachedMetadata | null
|
||||
): { name: RecognitionStrategy; criteria: string } | null {
|
||||
const config = this.config.getConfig();
|
||||
|
||||
if (config.recognitionStrategies.metadata.enabled &&
|
||||
|
||||
if (config.recognitionStrategies.metadata.enabled &&
|
||||
this.matchesMetadataStrategy(filePath, fileContent, fileCache, config.recognitionStrategies.metadata)) {
|
||||
return { name: "metadata", criteria: "frontmatter" };
|
||||
}
|
||||
|
||||
if (config.recognitionStrategies.tags.enabled &&
|
||||
|
||||
if (config.recognitionStrategies.tags.enabled &&
|
||||
this.matchesTagStrategy(filePath, fileContent, fileCache, config.recognitionStrategies.tags)) {
|
||||
return { name: "tag", criteria: "file-tags" };
|
||||
}
|
||||
|
||||
|
||||
// Check path strategy
|
||||
if (config.recognitionStrategies.paths.enabled &&
|
||||
if (config.recognitionStrategies.paths.enabled &&
|
||||
this.matchesPathStrategy(filePath, fileContent, fileCache, config.recognitionStrategies.paths)) {
|
||||
return { name: "path", criteria: config.recognitionStrategies.paths.taskPaths.join(", ") };
|
||||
}
|
||||
|
||||
|
||||
// TODO: Add template strategy in Phase 2
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -556,27 +565,28 @@ export class FileSource {
|
|||
): string {
|
||||
const config = this.config.getConfig().fileTaskProperties;
|
||||
const fileName = filePath.split('/').pop() || filePath;
|
||||
|
||||
const fileNameWithoutExt = fileName.replace(/\.[^/.]+$/, "");
|
||||
|
||||
switch (config.contentSource) {
|
||||
case 'filename':
|
||||
return config.stripExtension ?
|
||||
fileName.replace(/\.[^/.]+$/, "") : fileName;
|
||||
|
||||
return config.stripExtension ? fileNameWithoutExt : fileName;
|
||||
|
||||
case 'title':
|
||||
return fileCache?.frontmatter?.title || fileName;
|
||||
|
||||
// Always prefer frontmatter title if available, fallback to filename without extension
|
||||
return fileCache?.frontmatter?.title || fileNameWithoutExt;
|
||||
|
||||
case 'h1':
|
||||
const h1 = fileCache?.headings?.find(h => h.level === 1);
|
||||
return h1?.heading || fileName;
|
||||
|
||||
return h1?.heading || fileNameWithoutExt;
|
||||
|
||||
case 'custom':
|
||||
if (config.customContentField && fileCache?.frontmatter) {
|
||||
return fileCache.frontmatter[config.customContentField] || fileName;
|
||||
return fileCache.frontmatter[config.customContentField] || fileNameWithoutExt;
|
||||
}
|
||||
return fileName;
|
||||
|
||||
return fileNameWithoutExt;
|
||||
|
||||
default:
|
||||
return fileName;
|
||||
return config.stripExtension ? fileNameWithoutExt : fileName;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -591,12 +601,12 @@ export class FileSource {
|
|||
): Partial<FileSourceTaskMetadata> {
|
||||
const config = this.config.getConfig();
|
||||
const frontmatter = fileCache?.frontmatter || {};
|
||||
|
||||
|
||||
// Get status from frontmatter and map it to symbol if needed
|
||||
let status = frontmatter.status || config.fileTaskProperties.defaultStatus;
|
||||
|
||||
|
||||
// Apply status mapping if enabled
|
||||
if (config.statusMapping.enabled && frontmatter.status) {
|
||||
if (config.statusMapping && config.statusMapping.enabled && frontmatter.status) {
|
||||
// Try to map the metadata value to a symbol
|
||||
const mappedStatus = this.config.mapMetadataToSymbol(frontmatter.status);
|
||||
if (mappedStatus !== frontmatter.status) {
|
||||
|
|
@ -605,7 +615,7 @@ export class FileSource {
|
|||
console.log(`[FileSource] Mapped status '${frontmatter.status}' to '${mappedStatus}' for ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Extract standard task metadata
|
||||
const metadata: Partial<FileSourceTaskMetadata> = {
|
||||
dueDate: this.parseDate(frontmatter.dueDate || frontmatter.due),
|
||||
|
|
@ -619,7 +629,7 @@ export class FileSource {
|
|||
status: status,
|
||||
children: []
|
||||
};
|
||||
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
|
|
@ -629,34 +639,34 @@ export class FileSource {
|
|||
*/
|
||||
private mapSymbolToFileMetadata(symbol: string): string {
|
||||
const config = this.config.getConfig();
|
||||
|
||||
|
||||
if (!config.statusMapping.enabled) {
|
||||
return symbol;
|
||||
}
|
||||
|
||||
|
||||
// Map symbol back to preferred metadata value
|
||||
return this.config.mapSymbolToMetadata(symbol);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse date from various formats
|
||||
*/
|
||||
private parseDate(dateValue: any): number | undefined {
|
||||
if (!dateValue) return undefined;
|
||||
|
||||
|
||||
if (typeof dateValue === 'number') {
|
||||
return dateValue;
|
||||
}
|
||||
|
||||
|
||||
if (typeof dateValue === 'string') {
|
||||
const parsed = Date.parse(dateValue);
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
}
|
||||
|
||||
|
||||
if (dateValue instanceof Date) {
|
||||
return dateValue.getTime();
|
||||
}
|
||||
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -665,7 +675,7 @@ export class FileSource {
|
|||
*/
|
||||
private updateFileTaskCache(filePath: string, task: Task<FileSourceTaskMetadata>): void {
|
||||
const frontmatterHash = this.generateFrontmatterHash(filePath);
|
||||
|
||||
|
||||
this.fileTaskCache.set(filePath, {
|
||||
fileTaskExists: true,
|
||||
frontmatterHash,
|
||||
|
|
@ -680,10 +690,10 @@ export class FileSource {
|
|||
private generateFrontmatterHash(filePath: string): string {
|
||||
const file = this.app.vault.getAbstractFileByPath(filePath) as TFile;
|
||||
if (!file) return '';
|
||||
|
||||
|
||||
const fileCache = this.app.metadataCache.getFileCache(file);
|
||||
if (!fileCache?.frontmatter) return '';
|
||||
|
||||
|
||||
// Simple hash of frontmatter JSON
|
||||
const frontmatterStr = JSON.stringify(fileCache.frontmatter, Object.keys(fileCache.frontmatter).sort());
|
||||
return this.simpleHash(frontmatterStr);
|
||||
|
|
@ -724,19 +734,19 @@ export class FileSource {
|
|||
* Emit file task update event
|
||||
*/
|
||||
private emitFileTaskUpdate(
|
||||
action: 'created' | 'updated' | 'removed',
|
||||
action: 'created' | 'updated' | 'removed',
|
||||
task: Task<FileSourceTaskMetadata>
|
||||
): void {
|
||||
const seq = Seq.next();
|
||||
this.lastUpdateSeq = seq;
|
||||
|
||||
|
||||
emit(this.app, Events.FILE_TASK_UPDATED, {
|
||||
action,
|
||||
task,
|
||||
timestamp: Date.now(),
|
||||
seq
|
||||
});
|
||||
|
||||
|
||||
console.log(`[FileSource] File task ${action}: ${task.filePath}`);
|
||||
}
|
||||
|
||||
|
|
@ -744,26 +754,21 @@ export class FileSource {
|
|||
* Check if file is relevant for processing
|
||||
*/
|
||||
private isRelevantFile(filePath: string): boolean {
|
||||
const config = this.config.getConfig().advanced;
|
||||
|
||||
// Check exclude patterns
|
||||
for (const pattern of config.excludePatterns) {
|
||||
// Simple pattern matching (could be enhanced with glob patterns in Phase 2)
|
||||
if (filePath.includes(pattern.replace('**/', '').replace('/**', ''))) {
|
||||
return false;
|
||||
}
|
||||
// Use centralized FileFilterManager for 'file' scope filtering if available
|
||||
if (this.fileFilterManager && !this.fileFilterManager.shouldIncludePath(filePath, "file")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only process markdown files for now
|
||||
|
||||
// Only process markdown files for now (additional file type support can be added later)
|
||||
if (!filePath.endsWith('.md')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Skip system/hidden files
|
||||
if (filePath.startsWith('.') || filePath.includes('/.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -772,18 +777,24 @@ export class FileSource {
|
|||
*/
|
||||
private async performInitialScan(): Promise<void> {
|
||||
console.log("[FileSource] Performing initial scan...");
|
||||
|
||||
|
||||
const mdFiles = this.app.vault.getMarkdownFiles();
|
||||
console.log(`[FileSource] Found ${mdFiles.length} markdown files to check`);
|
||||
|
||||
let scannedCount = 0;
|
||||
let taskCount = 0;
|
||||
|
||||
let relevantCount = 0;
|
||||
|
||||
for (const file of mdFiles) {
|
||||
if (this.isRelevantFile(file.path)) {
|
||||
relevantCount++;
|
||||
try {
|
||||
const shouldBeTask = await this.shouldCreateFileTask(file.path);
|
||||
if (shouldBeTask) {
|
||||
await this.createFileTask(file.path);
|
||||
taskCount++;
|
||||
const task = await this.createFileTask(file.path);
|
||||
if (task) {
|
||||
taskCount++;
|
||||
}
|
||||
}
|
||||
scannedCount++;
|
||||
} catch (error) {
|
||||
|
|
@ -791,8 +802,22 @@ export class FileSource {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[FileSource] Initial scan complete: ${scannedCount} files scanned, ${taskCount} file tasks created`);
|
||||
|
||||
console.log(`[FileSource] Initial scan complete: ${mdFiles.length} total files, ${relevantCount} relevant, ${scannedCount} scanned, ${taskCount} file tasks created`);
|
||||
|
||||
if (taskCount === 0 && relevantCount > 0) {
|
||||
console.log(`[FileSource] No file tasks created. Check if your files match the configured recognition strategies:`);
|
||||
const config = this.config.getConfig();
|
||||
if (config.recognitionStrategies.metadata.enabled) {
|
||||
console.log(`[FileSource] - Metadata strategy: requires frontmatter with fields: ${config.recognitionStrategies.metadata.taskFields.join(', ')}`);
|
||||
}
|
||||
if (config.recognitionStrategies.tags.enabled) {
|
||||
console.log(`[FileSource] - Tag strategy: requires tags: ${config.recognitionStrategies.tags.taskTags.join(', ')}`);
|
||||
}
|
||||
if (config.recognitionStrategies.paths.enabled) {
|
||||
console.log(`[FileSource] - Path strategy: requires files in paths: ${config.recognitionStrategies.paths.taskPaths.join(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -804,13 +829,13 @@ export class FileSource {
|
|||
this.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (newConfig.enabled && !this.isInitialized) {
|
||||
// FileSource is being enabled
|
||||
this.initialize();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Configuration changed while active - might need to rescan
|
||||
// This is a Phase 2 enhancement
|
||||
console.log("[FileSource] Configuration updated");
|
||||
|
|
@ -838,19 +863,26 @@ export class FileSource {
|
|||
this.config.updateConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for updateConfiguration to match expected interface
|
||||
*/
|
||||
updateConfig(config: Partial<FileSourceConfiguration>): void {
|
||||
this.updateConfiguration(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force refresh of all file tasks
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
if (!this.isInitialized || !this.config.isEnabled()) return;
|
||||
|
||||
|
||||
console.log("[FileSource] Manual refresh triggered");
|
||||
|
||||
|
||||
// Clear cache and re-scan
|
||||
this.fileTaskCache.clear();
|
||||
this.stats.trackedFileCount = 0;
|
||||
this.stats.recognitionBreakdown = { metadata: 0, tag: 0, template: 0, path: 0 };
|
||||
|
||||
|
||||
await this.performInitialScan();
|
||||
}
|
||||
|
||||
|
|
@ -859,24 +891,24 @@ export class FileSource {
|
|||
*/
|
||||
destroy(): void {
|
||||
if (!this.isInitialized) return;
|
||||
|
||||
|
||||
console.log("[FileSource] Destroying FileSource...");
|
||||
|
||||
|
||||
// Clear all debouncing timeouts
|
||||
for (const timeout of this.pendingUpdates.values()) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
this.pendingUpdates.clear();
|
||||
|
||||
|
||||
// Clear event listeners
|
||||
for (const ref of this.eventRefs) {
|
||||
this.app.vault.offref(ref);
|
||||
}
|
||||
this.eventRefs = [];
|
||||
|
||||
|
||||
// Clear cache
|
||||
this.fileTaskCache.clear();
|
||||
|
||||
|
||||
// Reset statistics
|
||||
this.stats = {
|
||||
initialized: false,
|
||||
|
|
@ -885,7 +917,7 @@ export class FileSource {
|
|||
lastUpdate: 0,
|
||||
lastUpdateSeq: 0
|
||||
};
|
||||
|
||||
|
||||
// Emit cleanup event
|
||||
emit(this.app, Events.FILE_TASK_REMOVED, {
|
||||
filePath: null, // Indicates all file tasks removed
|
||||
|
|
@ -893,9 +925,35 @@ export class FileSource {
|
|||
seq: Seq.next(),
|
||||
destroyed: true
|
||||
});
|
||||
|
||||
|
||||
this.isInitialized = false;
|
||||
|
||||
|
||||
console.log("[FileSource] Cleanup complete");
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources and stop listening to events
|
||||
*/
|
||||
cleanup(): void {
|
||||
// Unsubscribe from all events
|
||||
this.eventRefs.forEach(ref => {
|
||||
if (this.app.workspace && typeof this.app.workspace.offref === 'function') {
|
||||
this.app.workspace.offref(ref);
|
||||
}
|
||||
});
|
||||
this.eventRefs = [];
|
||||
|
||||
// Clear pending updates
|
||||
this.pendingUpdates.forEach(timeout => clearTimeout(timeout));
|
||||
this.pendingUpdates.clear();
|
||||
|
||||
// Clear cache
|
||||
this.fileTaskCache.clear();
|
||||
|
||||
// Reset state
|
||||
this.isInitialized = false;
|
||||
this.stats.initialized = false;
|
||||
|
||||
console.log("[FileSource] Cleaned up and stopped");
|
||||
}
|
||||
}
|
||||
15
src/types/file-source.d.ts
vendored
15
src/types/file-source.d.ts
vendored
|
|
@ -94,6 +94,8 @@ export interface FileTaskPropertiesConfig {
|
|||
defaultStatus: string;
|
||||
/** Default priority for new file tasks */
|
||||
defaultPriority?: number;
|
||||
/** Prefer frontmatter title over file renaming when updating task content */
|
||||
preferFrontmatterTitle: boolean;
|
||||
}
|
||||
|
||||
/** Relationship configuration */
|
||||
|
|
@ -116,16 +118,6 @@ export interface PerformanceConfig {
|
|||
cacheTTL: number;
|
||||
}
|
||||
|
||||
/** Advanced configuration */
|
||||
export interface AdvancedConfig {
|
||||
/** Custom recognition function */
|
||||
customRecognitionFunction?: string;
|
||||
/** Exclude patterns */
|
||||
excludePatterns: string[];
|
||||
/** Maximum file size to process (bytes) */
|
||||
maxFileSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for mapping between file metadata status values and task status symbols
|
||||
*/
|
||||
|
|
@ -165,9 +157,6 @@ export interface FileSourceConfiguration {
|
|||
/** Performance configuration */
|
||||
performance: PerformanceConfig;
|
||||
|
||||
/** Advanced configuration */
|
||||
advanced: AdvancedConfig;
|
||||
|
||||
/** Status mapping configuration */
|
||||
statusMapping: StatusMappingConfig;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue