feat(status): extend multi-cycle support to all task switching contexts

- Add multi-cycle status support in ReadModeTextMark with getNextStatusPrimary
- Integrate getAllMarks in status-cycler for multi-cycle mark validation
- Update status-switcher to use multi-cycle configuration with fallback
- Maintain backward compatibility with legacy single-cycle mode
- Ensure consistent status cycling behavior across reading and editing views
This commit is contained in:
Quorafind 2025-11-19 12:48:30 +08:00
parent c82155815e
commit 689419538b
3 changed files with 156 additions and 46 deletions

View file

@ -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) ||

View file

@ -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 =

View file

@ -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;