From 9f600b9e21766ca864c2444ba351ccfda0367e50 Mon Sep 17 00:00:00 2001 From: HeroBlackInk Date: Tue, 17 Mar 2026 22:17:36 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Full=20Vault=20Sync=20typing=20hijack=20?= =?UTF-8?q?=E2=80=94=20defer=20new=20task=20creation=20to=20line=20leave?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CHANGELOG.md | 7 +++ manifest.json | 2 +- package.json | 2 +- src/plugin/eventHandlers.ts | 3 +- src/sync/toTodoist.ts | 91 +++++++++++++++++++++++++++++++++++-- versions.json | 3 +- 6 files changed, 99 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b42d520..86b95bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/manifest.json b/manifest.json index 4fec438..f3a7b85 100644 --- a/manifest.json +++ b/manifest.json @@ -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", diff --git a/package.json b/package.json index 02bdf75..33054d9 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/plugin/eventHandlers.ts b/src/plugin/eventHandlers.ts index 7ece782..50e4514 100644 --- a/src/plugin/eventHandlers.ts +++ b/src/plugin/eventHandlers.ts @@ -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(); } diff --git a/src/sync/toTodoist.ts b/src/sync/toTodoist.ts index 732ddba..77a7c8e 100644 --- a/src/sync/toTodoist.ts +++ b/src/sync/toTodoist.ts @@ -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 { + 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 { if (!this.plugin.isPrimaryDevice()) { this.plugin.debugLog('[toTodoist] Push blocked: not primary device'); diff --git a/versions.json b/versions.json index 97bcf3b..c365c8a 100644 --- a/versions.json +++ b/versions.json @@ -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" }