feat: support toggle parent checkbox when complete

This commit is contained in:
quorafind 2025-03-12 15:16:55 +08:00
parent f8aa496bb1
commit 1c82f2992b
6 changed files with 188 additions and 37 deletions

3
.gitignore vendored
View file

@ -24,3 +24,6 @@ data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
# cursorrules
.cursorrules

View file

@ -7,6 +7,7 @@ import {
} from "@codemirror/state";
import { getTabSize } from "./utils";
import { taskStatusChangeAnnotation } from "./taskStatusSwitcher";
import TaskProgressBarPlugin from "./taskProgressBarIndex";
/**
* Creates an editor extension that automatically completes parent tasks when all child tasks are completed
@ -14,9 +15,12 @@ import { taskStatusChangeAnnotation } from "./taskStatusSwitcher";
* @param plugin The plugin instance
* @returns An editor extension that can be registered with the plugin
*/
export function autoCompleteParentExtension(app: App) {
export function autoCompleteParentExtension(
app: App,
plugin: TaskProgressBarPlugin
) {
return EditorState.transactionFilter.of((tr) => {
return handleAutoCompleteParentTransaction(tr, app);
return handleAutoCompleteParentTransaction(tr, app, plugin);
});
}
@ -29,17 +33,14 @@ export function autoCompleteParentExtension(app: App) {
*/
export function handleAutoCompleteParentTransaction(
tr: Transaction,
app: App
app: App,
plugin: TaskProgressBarPlugin
): TransactionSpec {
// Only process transactions that change the document and are user input events
if (!tr.docChanged) {
return tr;
}
if (tr.annotation(taskStatusChangeAnnotation)) {
return tr;
}
// Check if a task was completed in this transaction
const taskCompletionInfo = findTaskCompletion(tr);
if (!taskCompletionInfo) {
@ -66,6 +67,28 @@ export function handleAutoCompleteParentTransaction(
return completeParentTask(tr, parentInfo.lineNumber, doc);
}
// If not all siblings are completed but some are, and the feature is enabled,
// mark the parent as "In Progress"
if (plugin.settings.markParentInProgressWhenPartiallyComplete) {
const anySiblingsCompleted = anySiblingCompleted(
doc,
parentInfo.lineNumber,
parentInfo.indentationLevel,
app
);
if (anySiblingsCompleted) {
const parentStatus = getParentTaskStatus(
doc,
parentInfo.lineNumber
);
// Only update if the parent is not already marked as "In Progress"
if (parentStatus !== ">" && parentStatus !== "/") {
return markParentAsInProgress(tr, parentInfo.lineNumber, doc);
}
}
}
return tr;
}
@ -288,5 +311,135 @@ function completeParentTask(
},
],
selection: tr.selection,
annotations: [taskStatusChangeAnnotation.of("taskStatusChange")],
};
}
/**
* Checks if any sibling tasks are completed
* @param doc The document to check
* @param parentLineNumber The line number of the parent task
* @param parentIndentLevel The indentation level of the parent task
* @param app The Obsidian app instance
* @returns True if any siblings are completed, false otherwise
*/
function anySiblingCompleted(
doc: Text,
parentLineNumber: number,
parentIndentLevel: number,
app: App
): boolean {
const tabSize = getTabSize(app);
// The expected indentation level for child tasks
const childIndentLevel = parentIndentLevel + tabSize;
// Track if we found at least one completed child
let foundCompletedChild = false;
// Search forward from the parent line
for (let i = parentLineNumber + 1; i <= doc.lines; i++) {
const line = doc.line(i);
const lineText = line.text;
// Skip empty lines
if (lineText.trim() === "") {
continue;
}
// Get the indentation of this line
const indentMatch = lineText.match(/^[\s|\t]*/);
const indentLevel = indentMatch ? indentMatch[0].length : 0;
// If we encounter a line with less or equal indentation to the parent,
// we've moved out of the parent's children scope
if (indentLevel <= parentIndentLevel) {
break;
}
// If this is a direct child of the parent (exactly one level deeper)
if (indentLevel === childIndentLevel) {
// Create a regex to match tasks based on the indentation level
const taskRegex = new RegExp(
`^[\\s|\\t]{${childIndentLevel}}([-*+]|\\d+\\.)\\s\\[(.)\\]`
);
const taskMatch = lineText.match(taskRegex);
if (taskMatch) {
// If we find a completed task, return true
const taskStatus = taskMatch[2];
console.log("taskStatus", taskStatus);
if (taskStatus !== " ") {
foundCompletedChild = true;
}
}
}
}
return foundCompletedChild;
}
/**
* Gets the current status of a parent task
* @param doc The document
* @param parentLineNumber The line number of the parent task
* @returns The task status character
*/
function getParentTaskStatus(doc: Text, parentLineNumber: number): string {
const parentLine = doc.line(parentLineNumber);
const parentLineText = parentLine.text;
// Find the task marker
const taskMarkerMatch = parentLineText.match(
/^[\s|\t]*([-*+]|\d+\.)\s\[(.)]/
);
if (!taskMarkerMatch) {
return "";
}
return taskMarkerMatch[2];
}
/**
* Marks a parent task as "In Progress" by modifying the transaction
* @param tr The transaction to modify
* @param parentLineNumber The line number of the parent task
* @param doc The document
* @returns The modified transaction
*/
function markParentAsInProgress(
tr: Transaction,
parentLineNumber: number,
doc: Text
): TransactionSpec {
const parentLine = doc.line(parentLineNumber);
const parentLineText = parentLine.text;
// Find the task marker position
const taskMarkerMatch = parentLineText.match(
/^[\s|\t]*([-*+]|\d+\.)\s\[( )\]/
);
if (!taskMarkerMatch) {
return tr;
}
// Calculate the position where we need to insert '>'
const markerStart =
parentLine.from +
taskMarkerMatch.index! +
taskMarkerMatch[0].indexOf("[ ]") +
1;
// Create a new transaction that adds the "In Progress" marker '>' to the parent task
return {
changes: [
tr.changes,
{
from: markerStart,
to: markerStart + 1,
insert: ">",
},
],
selection: tr.selection,
annotations: [taskStatusChangeAnnotation.of("taskStatusChange")],
};
}

View file

@ -64,7 +64,6 @@ function findTaskStatusChanges(tr: Transaction): {
const insertedText = inserted.toString();
// Debug log
console.log("Inserted text:", JSON.stringify(insertedText));
// Check if this is a new task creation with a newline
if (insertedText.includes("\n")) {
@ -85,10 +84,7 @@ function findTaskStatusChanges(tr: Transaction): {
const taskRegex = /^[\s|\t]*([-*+]|\d+\.)\s+\[(.)]/;
const match = originalLineText.match(taskRegex);
console.log("originalLineText", originalLineText, match);
if (match) {
console.log("Found task match:", match);
let changedPosition: number | null = null;
let currentMark: string | null = null;
let wasCompleteTask = false;
@ -103,7 +99,7 @@ function findTaskStatusChanges(tr: Transaction): {
// Get the mark position in the line
const markIndex = newLineText.indexOf("[") + 1;
changedPosition = newLine.from + markIndex;
console.log("changedPosition", changedPosition);
currentMark = match[2];
wasCompleteTask = true;
isTaskChange = true;
@ -114,7 +110,7 @@ function findTaskStatusChanges(tr: Transaction): {
const markIndex = newLineText.indexOf("[") + 1;
if (pos === newLine.from + markIndex) {
changedPosition = pos;
console.log("changedPosition", changedPosition);
currentMark = match[2];
wasCompleteTask = true;
isTaskChange = true;
@ -128,7 +124,7 @@ function findTaskStatusChanges(tr: Transaction): {
// Handle cases where part of a task including the mark was inserted
const markIndex = newLineText.indexOf("[") + 1;
changedPosition = newLine.from + markIndex;
console.log("changedPosition", changedPosition);
currentMark = match[2];
wasCompleteTask = true;
isTaskChange = true;
@ -182,11 +178,9 @@ export function handleCycleCompleteStatusTransaction(
return tr;
}
console.log("tr", tr);
// Check if any task statuses were changed in this transaction
const taskStatusChanges = findTaskStatusChanges(tr);
console.log("taskStatusChanges", taskStatusChanges);
if (taskStatusChanges.length === 0) {
return tr;
}
@ -194,17 +188,12 @@ export function handleCycleCompleteStatusTransaction(
// Get the task cycle and marks from plugin settings
const { cycle, marks } = getTaskStatusConfig(plugin);
console.log("cycle", cycle, "marks", marks);
// If no cycle is defined, don't do anything
if (cycle.length === 0) {
return tr;
}
// Log for debugging
console.log("Task status changes:", taskStatusChanges);
console.log("Task cycle:", cycle);
console.log("Task marks:", marks);
// Build a new list of changes to replace the original ones
const newChanges = [];
@ -233,8 +222,6 @@ export function handleCycleCompleteStatusTransaction(
const nextStatus = cycle[nextStatusIndex];
const nextMark = marks[nextStatus] || " ";
console.log("nextStatus", nextStatus, "nextMark", nextMark);
// Check if the current mark is the same as what would be the next mark in the cycle
// If they are the same, we don't need to process this further
if (currentMark === nextMark) {
@ -269,8 +256,6 @@ export function handleCycleCompleteStatusTransaction(
// Find the exact position to place the mark
const markPosition = position;
console.log("markPosition", markPosition, "nextMark", nextMark);
// Add a change to replace the current mark with the next one
newChanges.push({
from: markPosition,

View file

@ -118,7 +118,7 @@ export default class TaskProgressBarPlugin extends Plugin {
this.app.workspace.onLayoutReady(() => {
if (this.settings.autoCompleteParent) {
this.registerEditorExtension([
autoCompleteParentExtension(this.app),
autoCompleteParentExtension(this.app, this),
]);
}

View file

@ -10,6 +10,7 @@ export interface TaskProgressBarSettings {
addNumberToProgressBar: boolean;
showPercentage: boolean;
autoCompleteParent: boolean;
markParentInProgressWhenPartiallyComplete: boolean;
countSubLevel: boolean;
hideProgressBarBasedOnConditions: boolean;
hideProgressBarTags: string;
@ -56,6 +57,7 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
enableHeadingProgressBar: false,
addNumberToProgressBar: false,
autoCompleteParent: false,
markParentInProgressWhenPartiallyComplete: false,
showPercentage: false,
countSubLevel: true,
hideProgressBarBasedOnConditions: false,
@ -188,6 +190,24 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
})
);
new Setting(containerEl)
.setName("Mark parent as 'In Progress' when partially complete")
.setDesc(
"When some but not all child tasks are completed, mark the parent task as 'In Progress'. Only works when 'Auto complete parent' is enabled."
)
.addToggle((toggle) =>
toggle
.setValue(
this.plugin.settings
.markParentInProgressWhenPartiallyComplete
)
.onChange(async (value) => {
this.plugin.settings.markParentInProgressWhenPartiallyComplete =
value;
this.applySettingsUpdate();
})
);
this.showNumberToProgressbar();
new Setting(containerEl)

View file

@ -275,9 +275,6 @@ export function taskStatusSwitcherExtension(
}
const selection = view.state.selection;
if (selection.ranges.length === 1 && selection.ranges[0].empty) {
return this.isLivePreview(view.state);
}
const overlap = selection.ranges.some((r) => {
return !(r.to <= decorationFrom || r.from >= decorationTo);
@ -302,13 +299,6 @@ export function taskStatusSwitcherExtension(
const selection = plugin.view.state.selection;
if (
selection.ranges.length === 1 &&
selection.ranges[0].empty
) {
return true;
}
for (const range of selection.ranges) {
if (!(range.to <= rangeFrom || range.from >= rangeTo)) {
return false;