diff --git a/src/components/features/read-mode/ReadModeTextMark.ts b/src/components/features/read-mode/ReadModeTextMark.ts index 68631120..07294c70 100644 --- a/src/components/features/read-mode/ReadModeTextMark.ts +++ b/src/components/features/read-mode/ReadModeTextMark.ts @@ -3,12 +3,14 @@ import { Component, debounce, MarkdownPostProcessorContext, - setIcon, TFile, } from "obsidian"; import { getTasksAPI } from "@/utils"; import { parseTaskLine } from "@/utils/task/task-operations"; -import { getTaskStatusConfig } from "@/utils/status-cycle-resolver"; +import { + getTaskStatusConfig, + getNextStatusPrimary, +} from "@/utils/status-cycle-resolver"; // This component replaces standard checkboxes with custom text marks in reading view export function applyTaskTextMarks({ @@ -207,19 +209,53 @@ class TaskTextMark extends Component { (state) => !excludeMarksFromCycle.includes(state), ); - if (remainingCycle.length === 0) return; + let nextMark: string; + let nextState: string; + let currentState: string; - // Find current state in cycle - let currentState = - Object.keys(marks).find( - (state) => marks[state] === this.currentMark, - ) || remainingCycle[0]; + // Try to use multi-cycle configuration first + if ( + this.plugin.settings.statusCycles && + this.plugin.settings.statusCycles.length > 0 + ) { + const nextStatusResult = getNextStatusPrimary( + this.currentMark, + this.plugin.settings.statusCycles, + ); - // Find next state in cycle - const currentIndex = remainingCycle.indexOf(currentState); - const nextIndex = (currentIndex + 1) % remainingCycle.length; - const nextState = remainingCycle[nextIndex]; - const nextMark = marks[nextState] || " "; + if (nextStatusResult) { + nextState = nextStatusResult.statusName; + nextMark = nextStatusResult.mark; + // Find current state for Tasks API check + currentState = + Object.keys(marks).find( + (state) => marks[state] === this.currentMark, + ) || nextStatusResult.cycle.cycle[0]; + } else if (remainingCycle.length > 0) { + // If not found in multi-cycle, fall back to first status in current cycle + currentState = remainingCycle[0]; + nextState = remainingCycle[0]; + nextMark = marks[nextState] || " "; + } else { + // No cycle available + return; + } + } else { + // Fall back to legacy single-cycle logic + if (remainingCycle.length === 0) return; + + // Find current state in cycle + currentState = + Object.keys(marks).find( + (state) => marks[state] === this.currentMark, + ) || remainingCycle[0]; + + // Find next state in cycle + const currentIndex = remainingCycle.indexOf(currentState); + const nextIndex = (currentIndex + 1) % remainingCycle.length; + nextState = remainingCycle[nextIndex]; + nextMark = marks[nextState] || " "; + } // Check if next state is DONE and Tasks plugin is available const tasksApi = getTasksAPI(this.plugin); const canToggleWithTasksApi = nextState === "DONE" && !!tasksApi; @@ -342,7 +378,18 @@ class TaskTextMark extends Component { (state) => !excludeMarksFromCycle.includes(state), ); - if (remainingCycle.length === 0) return null; + // Only return null if: + // 1. No cycle is available (remainingCycle is empty) + // 2. AND multi-cycle mode is not enabled + if ( + remainingCycle.length === 0 && + !( + this.plugin.settings.statusCycles && + this.plugin.settings.statusCycles.length > 0 + ) + ) { + return null; + } let currentState: string = Object.keys(marks).find((state) => marks[state] === mark) || diff --git a/src/editor-extensions/task-operations/status-cycler.ts b/src/editor-extensions/task-operations/status-cycler.ts index fbb2aea5..8f017d78 100644 --- a/src/editor-extensions/task-operations/status-cycler.ts +++ b/src/editor-extensions/task-operations/status-cycler.ts @@ -15,6 +15,7 @@ import { getNextStatusPrimary, findApplicableCycles, getTaskStatusConfig, + getAllMarks, } from "@/utils/status-cycle-resolver"; /** @@ -59,9 +60,20 @@ function isValidTaskMarkerReplacement( return false; } - // Get valid task status marks from plugin settings - const { marks } = getTaskStatusConfig(plugin.settings); - const validMarks = Object.values(marks); + // Get valid task status marks from ALL enabled cycles (multi-cycle support) + let validMarks: string[]; + if ( + plugin.settings.statusCycles && + plugin.settings.statusCycles.length > 0 + ) { + // Multi-cycle mode: get all marks from all enabled cycles + const allMarksSet = getAllMarks(plugin.settings.statusCycles); + validMarks = Array.from(allMarksSet); + } else { + // Legacy single-cycle mode + const { marks } = getTaskStatusConfig(plugin.settings); + validMarks = Object.values(marks); + } // Check if both the original and inserted characters are valid task status marks const isOriginalValidMark = diff --git a/src/editor-extensions/task-operations/status-switcher.ts b/src/editor-extensions/task-operations/status-switcher.ts index e74a8058..64618f65 100644 --- a/src/editor-extensions/task-operations/status-switcher.ts +++ b/src/editor-extensions/task-operations/status-switcher.ts @@ -283,7 +283,7 @@ class TaskStatusWidget extends WidgetType { if (!currentMarkMatch) return; // Find the mark for the given status, checking multi-cycle config first - let nextMark: string = " "; // Default to space + let nextMark = " "; // Default to space if ( this.plugin.settings.statusCycles && @@ -364,38 +364,81 @@ class TaskStatusWidget extends WidgetType { (state) => !excludeMarksFromCycle.includes(state), ); - if (remainingCycle.length === 0) { - const editor = this.view.state.field(editorInfoField); - if (editor) { - editor?.editor?.cm?.dispatch({ - selection: EditorSelection.range(this.to + 1, this.to + 1), - }); - } - // If no cycle is available, trigger the default editor:toggle-checklist-status command - this.app.commands.executeCommandById( - "editor:toggle-checklist-status", + let nextMark: string; + let nextState: string; + + // Try to use multi-cycle configuration first + if ( + this.plugin.settings.statusCycles && + this.plugin.settings.statusCycles.length > 0 + ) { + const nextStatusResult = getNextStatusPrimary( + currentMark, + this.plugin.settings.statusCycles, ); - return; - } - let currentStateIndex = -1; - - for (let i = 0; i < remainingCycle.length; i++) { - const state = remainingCycle[i]; - if (marks[state] === currentMark) { - currentStateIndex = i; - break; + if (nextStatusResult) { + nextState = nextStatusResult.statusName; + nextMark = nextStatusResult.mark; + } else if (remainingCycle.length > 0) { + // If not found in multi-cycle, fall back to first status in current cycle + nextState = remainingCycle[0]; + nextMark = marks[nextState] || " "; + } else { + // No cycle available, trigger default Obsidian command + const editor = this.view.state.field(editorInfoField); + if (editor) { + editor?.editor?.cm?.dispatch({ + selection: EditorSelection.range( + this.to + 1, + this.to + 1, + ), + }); + } + this.app.commands.executeCommandById( + "editor:toggle-checklist-status", + ); + return; + } + } else { + // Fall back to legacy single-cycle logic + if (remainingCycle.length === 0) { + // No cycle available, trigger default Obsidian command + const editor = this.view.state.field(editorInfoField); + if (editor) { + editor?.editor?.cm?.dispatch({ + selection: EditorSelection.range( + this.to + 1, + this.to + 1, + ), + }); + } + this.app.commands.executeCommandById( + "editor:toggle-checklist-status", + ); + return; } - } - if (currentStateIndex === -1) { - currentStateIndex = 0; - } + let currentStateIndex = -1; - // Calculate next state - const nextStateIndex = (currentStateIndex + 1) % remainingCycle.length; - const nextState = remainingCycle[nextStateIndex]; - const nextMark = marks[nextState] || " "; + for (let i = 0; i < remainingCycle.length; i++) { + const state = remainingCycle[i]; + if (marks[state] === currentMark) { + currentStateIndex = i; + break; + } + } + + if (currentStateIndex === -1) { + currentStateIndex = 0; + } + + // Calculate next state + const nextStateIndex = + (currentStateIndex + 1) % remainingCycle.length; + nextState = remainingCycle[nextStateIndex]; + nextMark = marks[nextState] || " "; + } // Replace text const newText = currentText.replace(/\[(.)]/, `[${nextMark}]`); @@ -446,9 +489,17 @@ export function taskStatusSwitcherExtension( (state) => !excludeMarksFromCycle.includes(state), ); + // Only skip rendering if: + // 1. No cycle is available (remainingCycle is empty) + // 2. AND multi-cycle mode is not enabled + // 3. AND custom task marks are not enabled if ( remainingCycle.length === 0 && - !plugin.settings.enableCustomTaskMarks + !plugin.settings.enableCustomTaskMarks && + !( + plugin.settings.statusCycles && + plugin.settings.statusCycles.length > 0 + ) ) return;