fix: Full Vault Sync typing hijack — defer new task creation to line leave

Previously, lineContentNewTaskCheck fired on every keystroke in Full Vault
Sync mode, creating a Todoist task after typing a single character.

Now new task detection in Full Vault Sync is handled by lastLineNewTaskCheck,
which only fires when the cursor leaves the line. lineContentNewTaskCheck is
narrowed to only handle manually-tagged #todoist tasks.

Fixes #110 (problem 4)
This commit is contained in:
HeroBlackInk 2026-03-17 22:17:36 +08:00
parent 799bd13a71
commit 9f600b9e21
6 changed files with 99 additions and 9 deletions

View file

@ -1,5 +1,12 @@
## CHANGELOG
### [2.0.2] - 2026-03-17
#### Fixed
- Full Vault Sync: new tasks are now created only when the cursor leaves the line, preventing the "typing hijack" bug where every keystroke triggered task creation
---
### [2.0.0] - 2026-03-15
#### Added

View file

@ -1,7 +1,7 @@
{
"id": "ultimate-todoist-sync",
"name": "Ultimate Todoist Sync",
"version": "2.0.1",
"version": "2.0.2",
"minAppVersion": "1.0.0",
"description": "This is the best Todoist task synchronization plugin for Obsidian so far.",
"author": "HeroBlackInk",

View file

@ -1,6 +1,6 @@
{
"name": "ultimate-todoist-sync",
"version": "2.0.1",
"version": "2.0.2",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {

View file

@ -166,8 +166,9 @@ export class EventHandlers {
if (!await this.plugin.syncLockManager.acquire('obsidianToTodoist')) return;
try {
await this.plugin.obsidianToTodoist!.lineModifiedTaskCheck(filepath as string, lastLineText, lastLine as number, fileContent);
await this.plugin.obsidianToTodoist!.lastLineNewTaskCheck(filepath as string, lastLineText, lastLine as number, fileContent);
} catch (error) {
console.error(`An error occurred while check modified task in line text: ${error}`);
console.error(`An error occurred while checking task on line leave: ${error}`);
} finally {
this.plugin.syncLockManager.release();
}

View file

@ -88,11 +88,7 @@ export class ObsidianToTodoistSync {
const hasId = this.plugin.taskParser.hasTodoistId(linetxt);
const hasTag = this.plugin.taskParser.hasTodoistTag(linetxt);
const fullVault = this.plugin.settings.enableFullVaultSync;
const isTask = this.plugin.taskParser.isMarkdownTask(linetxt);
const contentNotEmpty = isTask && this.plugin.taskParser.getTaskContentFromLineText(linetxt) !== "";
const isNewTask = !hasId && (hasTag || (fullVault && contentNotEmpty));
const isNewTask = !hasId && hasTag;
if (isNewTask) {
const processedLine = hasTag ? linetxt : this.plugin.taskParser.addTodoistTag(linetxt);
@ -169,6 +165,91 @@ export class ObsidianToTodoistSync {
}
}
/**
* Detect new tasks when the user leaves a line (Full Vault Sync only).
* Unlike lineContentNewTaskCheck (which fires on every keystroke for #todoist),
* this fires once when the cursor moves away, so the user can finish typing.
*/
async lastLineNewTaskCheck(filepath: string, lineText: string, lineNumber: number, fileContent: string): Promise<void> {
if (!this.plugin.isPrimaryDevice()) return;
if (!this.plugin.settings.enableFullVaultSync) return;
const isTask = this.plugin.taskParser.isMarkdownTask(lineText);
if (!isTask) return;
const contentNotEmpty = this.plugin.taskParser.getTaskContentFromLineText(lineText) !== '';
if (!contentNotEmpty) return;
const hasId = this.plugin.taskParser.hasTodoistId(lineText);
if (hasId) return;
const hasTag = this.plugin.taskParser.hasTodoistTag(lineText);
if (hasTag) return; // Already has #todoist — lineContentNewTaskCheck will handle it
// Add #todoist tag
const processedLine = this.plugin.taskParser.addTodoistTag(lineText);
this.plugin.debugLog('[lastLineNewTaskCheck] New task detected on line leave:', processedLine);
const currentTask = await this.plugin.taskParser.convertTextToTodoistTaskObject(processedLine, filepath, lineNumber, fileContent);
if (typeof currentTask === 'undefined') return;
try {
const newTask = await this.plugin.todoistSyncAPI.AddTask(currentTask);
const { id: todoist_id } = newTask;
newTask.path = filepath;
new Notice(`new task ${newTask.content} id is ${newTask.id}`);
this.plugin.logOperation?.log('OBSIDIAN_TASK_CREATED', `Created task in Obsidian: ${newTask.content}`, filepath, todoist_id, 'obsidian\u2192todoist');
this.plugin.logOperation?.log('TODOIST_TASK_CREATED', `Created task in Todoist: ${newTask.content}`, filepath, todoist_id, 'obsidian\u2192todoist');
await this.plugin.cacheOperation.setTaskFileMapping(todoist_id, filepath || '');
// Immediately sync so syncData contains the new task
try {
await this.plugin.todoistSyncAPI.incrementalSync();
const updatedTask = await this.plugin.todoistSyncAPI.GetTaskById(todoist_id);
if (updatedTask?.updated_at) {
await this.plugin.cacheOperation.updateTaskMappingSyncMeta(todoist_id, { updated_at: updatedTask.updated_at });
}
} catch (syncErr) {
console.error('[lastLineNewTaskCheck] Post-create incremental sync failed:', syncErr);
}
if (currentTask.isCompleted === true) {
await this.plugin.todoistSyncAPI.CloseTask(newTask.id);
this.plugin.logOperation?.log('OBSIDIAN_TASK_COMPLETED', `Completed task in Obsidian: ${newTask.content}`, filepath, todoist_id, 'obsidian\u2192todoist');
this.plugin.logOperation?.log('TODOIST_TASK_COMPLETED', `Completed task in Todoist: ${newTask.content}`, filepath, todoist_id, 'obsidian\u2192todoist');
}
// Write tag + id + link back to the file
const text_with_out_link = `${processedLine} %%[todoist_id:: ${todoist_id}]%%`;
const link = this.plugin.settings.useAppURI ? `[link](todoist://task?id=${newTask.id})` : `[link](https://app.todoist.com/app/task/${newTask.id})`;
const text = this.plugin.taskParser.addTodoistLink(text_with_out_link, link);
// Use vault.read + vault.modify since cursor is no longer on this line
const file = this.app.vault.getAbstractFileByPath(filepath);
if (!file) {
console.error(`[lastLineNewTaskCheck] File not found: ${filepath}`);
return;
}
const currentContent = await this.app.vault.read(file);
const lines = currentContent.split('\n');
if (lineNumber < lines.length) {
lines[lineNumber] = text;
await this.app.vault.modify(file, lines.join('\n'));
}
const saved = await this.plugin.saveSettings();
if (!saved) {
console.warn('[lastLineNewTaskCheck] saveSettings skipped or failed');
}
} catch (error) {
console.error('[lastLineNewTaskCheck] Error adding task:', error);
this.plugin.debugLog(`The error occurred in the file: ${filepath}`);
new Notice(`Failed to create task. Check console for details.`);
}
}
async fullTextNewTaskCheck(file_path: string): Promise<void> {
if (!this.plugin.isPrimaryDevice()) {
this.plugin.debugLog('[toTodoist] Push blocked: not primary device');

View file

@ -1,5 +1,6 @@
{
"1.0.0": "0.15.0",
"2.0.0": "1.0.0",
"2.0.1": "1.0.0"
"2.0.1": "1.0.0",
"2.0.2": "1.0.0"
}