mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat: support toggle parent checkbox when complete
This commit is contained in:
parent
f8aa496bb1
commit
1c82f2992b
6 changed files with 188 additions and 37 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -24,3 +24,6 @@ data.json
|
|||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
|
||||
# cursorrules
|
||||
.cursorrules
|
||||
|
|
|
|||
|
|
@ -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")],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue