Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task Cache Implementation #12

Merged
merged 11 commits into from
Aug 1, 2024
61 changes: 42 additions & 19 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {UserUser} from "./vikunja_sdk";
import {Label} from "./src/vikunja/labels";
import Commands from "./src/commands";
import {Projects} from "./src/vikunja/projects";
import VaultTaskCache from "./src/settings/VaultTaskCache";

// Remember to rename these classes and interfaces!

Expand All @@ -17,6 +18,7 @@ export default class VikunjaPlugin extends Plugin {
processor: Processor;
commands: Commands;
projectsApi: Projects;
cache: VaultTaskCache;

async onload() {
await this.loadSettings();
Expand All @@ -38,17 +40,21 @@ export default class VikunjaPlugin extends Plugin {

async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
this.cache = VaultTaskCache.fromJson(this.settings.cache, this.app, this);
}

async saveSettings() {
this.settings.cache = this.cache.getCachedTasks().map(task => {
return task.toJson();
});
await this.saveData(this.settings);
}

async checkLastLineForUpdate() {
if (this.settings.debugging) console.log("Checking for task update");
const updateTask = await this.processor.checkUpdateInLineAvailable()
if (!!updateTask) {
await this.tasksApi.updateTask(updateTask.task);
if (updateTask !== undefined) {
await this.tasksApi.updateTask(updateTask);
} else {
if (this.settings.debugging) console.log("No task to update found");
}
Expand All @@ -70,24 +76,14 @@ export default class VikunjaPlugin extends Plugin {
this.registerDomEvent(document, 'keyup', this.handleUpDownEvent.bind(this));
this.registerDomEvent(document, 'click', this.handleClickEvent.bind(this));
this.registerEvent(this.app.workspace.on('editor-change', this.handleEditorChange.bind(this)));

// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
this.registerInterval(window
.setInterval(async () => {
// this runs anyway, also when cron not enabled, to be dynamically enabled by settings without disable/enable plugin
if (this.settings.enableCron) {
await this.processor.exec()
}
},
this.settings.cronInterval * 1000)
);
}

private setupAPIs() {
this.tasksApi = new Tasks(this.app, this);
this.userObject = undefined;
this.labelsApi = new Label(this.app, this);
this.projectsApi = new Projects(this.app, this);
this.cache = new VaultTaskCache(this.app, this);
}

private setupCommands() {
Expand Down Expand Up @@ -122,19 +118,42 @@ export default class VikunjaPlugin extends Plugin {
})
}

private async handleEditorChange() {
if (this.settings.debugging) console.log("Editor changed");
private async handleEditorChange(data: any) {
if (this.settings.debugging) console.log("Editor changed", data);
const currentFile = this.app.workspace.getActiveFile();
if (!currentFile) {
if (this.settings.debugging) console.log("No file open");
return;
}

const tasks = await this.processor.getVaultSearcher().getTasksFromFile(this.processor.getTaskParser(), currentFile);
for (const task of tasks) {
if (task.task.id) {
const cachedTask = this.cache.get(task.task.id);
if (cachedTask === undefined || !cachedTask.isTaskEqual(task.task)) {
this.cache.update(task);
} else {
if (cachedTask.lineno !== task.lineno || cachedTask.filepath !== task.filepath) {
this.cache.updateFileInfos(task.task.id, task.filepath, task.lineno);
}
}
}
}
// FIXME the update line stuff should be communicated in settings
return;
//await this.checkLastLineForUpdate();
}

private async handleUpDownEvent(evt: KeyboardEvent) {
if (evt.key === 'ArrowUp' || evt.key === 'ArrowDown' || evt.key === 'PageUp' || evt.key === 'PageDown') {
if (evt.key === 'ArrowUp' || evt.key === 'ArrowDown' || evt.key === 'PageUp' || evt.key === 'PageDown' || evt.key === "Enter") {
if (this.settings.debugging) console.log("Line changed via keys");
await this.checkLastLineForUpdate();
}
}

private async handleClickEvent(evt: MouseEvent) {
if (!this.settings.updateOnCursorMovement) {
return;
}
const target = evt.target as HTMLInputElement;
if (this.app.workspace.activeEditor?.editor?.hasFocus()) {
await this.checkLastLineForUpdate();
Expand All @@ -153,8 +172,12 @@ export default class VikunjaPlugin extends Plugin {
const taskId = parseInt(match[1]);
if (this.settings.debugging) console.log("Checkbox clicked for task", taskId);
const task = await this.tasksApi.getTaskById(taskId);
task.done = target.checked;
await this.tasksApi.updateTask(task);
const cachedTask = this.cache.get(taskId);
if (cachedTask !== undefined) {
cachedTask.task = task;
cachedTask.task.done = target.checked;
await this.tasksApi.updateTask(cachedTask);
}
} else {
if (this.settings.debugging) console.log("No task id found for checkbox");
}
Expand Down
3 changes: 2 additions & 1 deletion src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default class Commands {
}
if (this.plugin.settings.debugging) console.log("Move all tasks to default project");

const tasks = (await this.getTasksFromVault()).filter(task => task.task.id !== undefined).map(task => task.task);
const tasks = (await this.getTasksFromVault()).filter(task => task.task.id !== undefined);
await this.plugin.tasksApi.updateProjectsIdInVikunja(tasks, this.plugin.settings.defaultVikunjaProject);
}

Expand Down Expand Up @@ -141,6 +141,7 @@ export default class Commands {

if (this.plugin.settings.debugging) console.log("Resetting tasks in Vikunja done");
await this.plugin.labelsApi.loadLabels();
this.plugin.cache.reset();
new Notice("Resetting tasks and labels in Vikunja done");
}
).open();
Expand Down
2 changes: 2 additions & 0 deletions src/processing/automaton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import UpdateTasks from "./updateTasks";
import CreateTasks from "./createTasks";
import {Processor} from "./processor";
import {SyncLabels} from "./syncLabels";
import CheckCache from "./checkCache";

interface StepsOutput {
localTasks: PluginTask[];
Expand Down Expand Up @@ -41,6 +42,7 @@ class Automaton {

this.steps = [
new GetTasks(app, plugin, processor),
new CheckCache(plugin, app, processor),
new SyncLabels(app, plugin),
new RemoveTasks(app, plugin),
new CreateTasks(app, plugin, processor),
Expand Down
41 changes: 41 additions & 0 deletions src/processing/checkCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {PluginTask} from "src/vaultSearcher/vaultSearcher";
import {ModelsTask} from "vikunja_sdk";
import {IAutomatonSteps, StepsOutput} from "./automaton";
import VikunjaPlugin from "../../main";
import {App} from "obsidian";
import {compareModelTasks, Processor} from "./processor";

export default class CheckCache implements IAutomatonSteps {
plugin: VikunjaPlugin;
app: App;
processor: Processor;

constructor(plugin: VikunjaPlugin, app: App, processor: Processor) {
this.plugin = plugin;
this.app = app;
this.processor = processor;
}

async step(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]): Promise<StepsOutput> {
this.updateCacheFromVault(localTasks);
return {localTasks, vikunjaTasks};
}

updateCacheFromVault(localTasks: PluginTask[]) {
const tasksWithId = localTasks.filter(task => {
if (task.task.id === undefined) return false; // task has no id, so it is not in the cache, because not synced to vikunja

const elem = this.plugin.cache.get(task.task.id)
if (elem === undefined) return false; // task is not in the cache, because not synced to vikunja

return !compareModelTasks(elem.task, task.task); // filter elem, if it is equal to task in cache. False filters out.
}
);

if (tasksWithId.length === 0) {
if (this.plugin.settings.debugging) console.log("Step CheckCache: No changes in vault found. Cache is up to date.");
return;
}
if (this.plugin.settings.debugging) console.log("Step CheckCache: Something changed the vault without obsidian! Invalidate cache and creating anew");
}
}
77 changes: 56 additions & 21 deletions src/processing/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,14 @@ class Processor {
async saveToVault(task: PluginTask) {
const newTask = this.getTaskContent(task);

await this.app.vault.process(task.file, data => {
const file = this.app.vault.getFileByPath(task.filepath);
if (file === null) {
return;
}
await this.app.vault.process(file, data => {
let content;
if (this.plugin.settings.appendMode) {
return data + "\n" + newTask;
content = data + "\n" + newTask;
} else {
const lines = data.split("\n");
for (let i = 0; i < lines.length; i++) {
Expand All @@ -86,8 +91,10 @@ class Processor {
break;
}
}
return lines.join("\n");
content = lines.join("\n");
}
this.plugin.cache.update(task);
return content;
});
}

Expand Down Expand Up @@ -169,37 +176,53 @@ class Processor {
}

const lastLine = this.lastLineChecked.get(currentFilename);
let pluginTask = undefined;
let updatedTask = undefined;
if (!!lastLine) {
const lastLineText = view.editor.getLine(lastLine);
if (this.plugin.settings.debugging) console.log("Processor: Last line,", lastLine, "Last line text", lastLineText);
try {
const parsedTask = await this.taskParser.parse(lastLineText);
pluginTask = {
file: file,
lineno: lastLine,
task: parsedTask
};
updatedTask = new PluginTask(file.path, lastLine, parsedTask);
if (updatedTask.task.id === undefined) {
return undefined;
}
const cacheTask = this.plugin.cache.get(updatedTask.task.id);
if (cacheTask === undefined) {
if (this.plugin.settings.debugging) console.error("Processor: Should not be here, because if this task is not in cache, but has an id, it circumvented the cache.")
return undefined;
}
if (compareModelTasks(updatedTask.task, cacheTask.task)) {
// Cache and current task are equal, so no update is needed
return undefined;
}
// no guard check fires, so there is an update.
this.plugin.cache.update(updatedTask);
} catch (e) {
if (this.plugin.settings.debugging) console.log("Processor: Error while parsing task", e);
}
}

this.lastLineChecked.set(currentFilename, currentLine);
return pluginTask;
return updatedTask;
}

/* Update a task in the vault
* If the second parameter set to false, the vikunja metadata will not entered. But per default, the metadata will be entered.
*/
async updateToVault(task: PluginTask, metadata: boolean = true) {
const newTask = (metadata) ? this.getTaskContent(task) : this.getTaskContentWithoutVikunja(task);

await this.app.vault.process(task.file, (data: string) => {
const file = this.app.vault.getFileByPath(task.filepath);
if (file === null) {
return;
}
await this.app.vault.process(file, (data: string) => {
const lines = data.split("\n");
lines.splice(task.lineno, 1, newTask);
return lines.join("\n");
const content = lines.join("\n");
this.plugin.cache.update(task);
return content;
});

}

getVaultSearcher(): VaultSearcher {
Expand Down Expand Up @@ -252,8 +275,12 @@ class Processor {
for (const task of tasksToPushToVault) {
let file: TFile;
const chosenFile = this.app.vault.getFileByPath(this.plugin.settings.chosenOutputFile);
// FIXME This should be the date of the vikunja created date, so the task is created in the correct daily note
const date = moment();
const formattedDate = task.created;
let date = moment();
if (formattedDate !== undefined) {
if (this.plugin.settings.debugging) console.log("Step CreateTask: Found formatted date", formattedDate, "using it as daily note");
date = moment(formattedDate, "YYYY-MM-DDTHH:mm:ss[Z]");
}
const dailies = getAllDailyNotes()

switch (this.plugin.settings.chooseOutputFile) {
Expand All @@ -275,11 +302,7 @@ class Processor {
default:
throw new Error("No valid chooseOutputFile selected");
}
const pluginTask: PluginTask = {
file: file,
lineno: 0,
task: task
};
const pluginTask = new PluginTask(file.path, 0, task);
createdTasksInVault.push(pluginTask);
}

Expand All @@ -301,7 +324,19 @@ class Processor {
await this.plugin.tasksApi.deleteTask(task.task);
}
}
}

function compareModelTasks(local: ModelsTask, vikunja: ModelsTask): boolean {
const title = local.title === vikunja.title;
const description = local.description === vikunja.description;
const dueDate = local.dueDate === vikunja.dueDate;
const labels = local.labels?.filter(label => vikunja.labels?.find(vikunjaLabel => vikunjaLabel.title === label.title)).length === local.labels?.length;
const priority = local.priority === vikunja.priority;
const status = local.done === vikunja.done;
const doneAt = local.doneAt === vikunja.doneAt;
const updated = local.updated === vikunja.updated;

return title && description && dueDate && labels && priority && status && doneAt && updated;
}

export {Processor};
export {Processor, compareModelTasks};
16 changes: 2 additions & 14 deletions src/processing/updateTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class UpdateTasks implements IAutomatonSteps {
if (this.plugin.settings.debugging) console.log("Step UpdateTask: updated field is not defined", task, vikunjaTask);
throw new Error("Task updated field is not defined");
}
if (this.areTasksEqual(task.task, vikunjaTask)) {
if (task.isTaskEqual(vikunjaTask)) {
if (this.plugin.settings.debugging) console.log("Step UpdateTask: Task is the same in both platforms", task, vikunjaTask);
continue;
}
Expand All @@ -79,16 +79,6 @@ class UpdateTasks implements IAutomatonSteps {
}

private areTasksEqual(local: ModelsTask, vikunja: ModelsTask) {
const title = local.title === vikunja.title;
const description = local.description === vikunja.description;
const dueDate = local.dueDate === vikunja.dueDate;
const labels = local.labels?.filter(label => vikunja.labels?.find(vikunjaLabel => vikunjaLabel.title === label.title)).length === local.labels?.length;
const priority = local.priority === vikunja.priority;
const status = local.done === vikunja.done;
const doneAt = local.doneAt === vikunja.doneAt;
// const updatedAt = local.updated === vikunja.updated; not usable, because it is different if anything changes in local file

return title && description && dueDate && labels && priority && status && doneAt;
}

private async updateTasks(tasksToUpdateInVault: PluginTask[], tasksToUpdateInVikunja: PluginTask[]) {
Expand All @@ -100,9 +90,7 @@ class UpdateTasks implements IAutomatonSteps {
private async updateTasksInVikunja(updateTasks: PluginTask[]) {
if (this.plugin.settings.debugging) console.log("Step UpdateTask: Update tasks in vikunja");

for (const task of updateTasks) {
await this.plugin.tasksApi.updateTask(task.task);
}
await Promise.all(updateTasks.map(task => this.plugin.tasksApi.updateTask(task)));
}

private async updateTasksInVault(updateTasks: PluginTask[]) {
Expand Down
Loading
Loading