Add heading-grouped, nested, draggable todos with keyboard navigation
Enhance the todo tracker sidebar panel with: - Group todos by heading from the source file - Display nested (indented) todos as a tree - Drag-and-drop to reorder todos within/across heading groups - Keyboard navigation (arrow keys, Enter to open, M to move, toggle hotkey) - Right-click context menu replacing the ellipsis button - Proper focus management (no focus stealing on refresh) - Platform-aware Obsidian hotkey matching for toggle checkbox Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,78 @@ export function insertTodoAtEnd(content: string, todoLine: string): string {
|
||||
return content + '\n' + todoLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a todo (and its children) from one position to another within the same content.
|
||||
* @param content - The file content
|
||||
* @param todoLineNumber - Line number of the todo to move
|
||||
* @param childLineNumbers - Line numbers of child todos (must be consecutive after todoLineNumber)
|
||||
* @param targetLineNumber - Line number to insert before (0-based). Lines are inserted at this position.
|
||||
*/
|
||||
export function moveTodoWithChildren(
|
||||
content: string,
|
||||
todoLineNumber: number,
|
||||
childLineNumbers: number[],
|
||||
targetLineNumber: number
|
||||
): string {
|
||||
const lines = content.split('\n');
|
||||
const allLineNumbers = [todoLineNumber, ...childLineNumbers].sort((a, b) => a - b);
|
||||
|
||||
// Extract the lines to move
|
||||
const movedLines = allLineNumbers.map((ln) => lines[ln]!);
|
||||
|
||||
// If moving to same position, no-op
|
||||
if (targetLineNumber === todoLineNumber) {
|
||||
return content;
|
||||
}
|
||||
|
||||
// Remove lines from original positions (in reverse to preserve indices)
|
||||
for (let i = allLineNumbers.length - 1; i >= 0; i--) {
|
||||
lines.splice(allLineNumbers[i]!, 1);
|
||||
}
|
||||
|
||||
// Adjust target position for removed lines
|
||||
let adjustedTarget = targetLineNumber;
|
||||
for (const ln of allLineNumbers) {
|
||||
if (ln < targetLineNumber) {
|
||||
adjustedTarget--;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert at adjusted target
|
||||
lines.splice(adjustedTarget, 0, ...movedLines);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a todo (and its children) under a heading within the same file.
|
||||
* Inserts at the end of the heading's section.
|
||||
* @param content - The file content
|
||||
* @param todoLineNumber - Line number of the todo to move
|
||||
* @param childLineNumbers - Line numbers of child todos
|
||||
* @param headingLineNumber - Line number of the target heading
|
||||
* @param nextHeadingLineNumber - Optional line number of the next heading (section boundary)
|
||||
*/
|
||||
export function moveTodoUnderHeadingInFile(
|
||||
content: string,
|
||||
todoLineNumber: number,
|
||||
childLineNumbers: number[],
|
||||
headingLineNumber: number,
|
||||
nextHeadingLineNumber?: number
|
||||
): string {
|
||||
// Determine insert position: end of section (before next heading) or after heading
|
||||
let targetLineNumber: number;
|
||||
if (nextHeadingLineNumber !== undefined) {
|
||||
targetLineNumber = nextHeadingLineNumber;
|
||||
} else {
|
||||
// No next heading — insert at end of file
|
||||
const lines = content.split('\n');
|
||||
targetLineNumber = lines.length;
|
||||
}
|
||||
|
||||
return moveTodoWithChildren(content, todoLineNumber, childLineNumbers, targetLineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a todo line under a specific heading.
|
||||
* @param content - The file content
|
||||
|
||||
Reference in New Issue
Block a user