Skip to content

Commit

Permalink
Merge pull request #12 from Heiss/implement-cache
Browse files Browse the repository at this point in the history
Task Cache Implementation
  • Loading branch information
Heiss authored Aug 1, 2024
2 parents 3166703 + b51b2be commit 3bc06f5
Show file tree
Hide file tree
Showing 11 changed files with 525 additions and 192 deletions.
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

0 comments on commit 3bc06f5

Please sign in to comment.