mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
refactor: improve task parsing, date management, and tag handling
- Enhanced task parser to preserve trailing spaces for empty-content tasks - Refactored WriteAPI for cleaner date insertion logic with proper formatting - Improved status cycler to prevent conflicts with normal typing - Fixed escaped hash tag handling to align with Obsidian behavior - Optimized tag extraction with recursive unprotected hash detection - Added CodeMirror state mock for better test coverage - Improved quadrant card component structure and rendering
This commit is contained in:
parent
c75f207a2d
commit
cf51bdb758
9 changed files with 620 additions and 238 deletions
|
|
@ -2,6 +2,8 @@ module.exports = {
|
|||
preset: "ts-jest",
|
||||
testEnvironment: "jsdom",
|
||||
testMatch: ["**/__tests__/**/*.test.ts"],
|
||||
testPathIgnorePatterns: ["<rootDir>/.conductor/"],
|
||||
modulePathIgnorePatterns: ["<rootDir>/.conductor/"],
|
||||
moduleNameMapper: {
|
||||
"^obsidian$": "<rootDir>/src/__mocks__/obsidian.ts",
|
||||
"^moment$": "<rootDir>/src/__mocks__/moment.js",
|
||||
|
|
|
|||
|
|
@ -186,6 +186,37 @@ export class EditorState {
|
|||
}
|
||||
}
|
||||
|
||||
update(spec: any = {}) {
|
||||
const changesSpec = spec.changes || {};
|
||||
const from = changesSpec.from ?? 0;
|
||||
const to = changesSpec.to ?? from;
|
||||
const insert =
|
||||
typeof changesSpec.insert === "string"
|
||||
? changesSpec.insert
|
||||
: changesSpec.insert?.toString?.() ?? "";
|
||||
|
||||
const oldText = this.doc.toString();
|
||||
const newContent = oldText.slice(0, from) + insert + oldText.slice(to);
|
||||
const newDoc = new Text(newContent);
|
||||
|
||||
const changes = new Changes();
|
||||
(changes as any)._changes.push({
|
||||
fromA: from,
|
||||
toA: to,
|
||||
fromB: from,
|
||||
toB: from + insert.length,
|
||||
inserted: new Text(insert),
|
||||
});
|
||||
|
||||
return new Transaction({
|
||||
startState: this,
|
||||
newDoc,
|
||||
changes,
|
||||
selection: spec.selection,
|
||||
annotations: spec.annotations,
|
||||
});
|
||||
}
|
||||
|
||||
field<T>(field: any /* StateField<T> | Facet<any, T> */): T | undefined {
|
||||
return this._fields.get(field);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ describe("Task Mark Cleanup", () => {
|
|||
|
||||
test("should handle complex example from user", () => {
|
||||
const input = "今天要过去吃饭 #123-123-123 ~ 📅 2025-07-18";
|
||||
const expected = "今天要过去吃饭 #123-123-123 2025-07-18";
|
||||
const expected = "今天要过去吃饭 2025-07-18"; // #123-123-123 is a normal tag and should be removed
|
||||
expect(clearAllMarks(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -218,7 +218,8 @@ export class QuadrantCardComponent extends Component {
|
|||
numericPriority = this.task.metadata.priority;
|
||||
}
|
||||
|
||||
const sanitizedPriority = sanitizePriorityForClass(numericPriority);
|
||||
const sanitizedPriority =
|
||||
sanitizePriorityForClass(numericPriority);
|
||||
const classes = ["tg-quadrant-card-priority"];
|
||||
if (sanitizedPriority) {
|
||||
classes.push(`priority-${sanitizedPriority}`);
|
||||
|
|
@ -422,8 +423,50 @@ export class QuadrantCardComponent extends Component {
|
|||
}
|
||||
|
||||
private extractTags(): string[] {
|
||||
const tags = this.task.content.match(/#[\w-]+/g) || [];
|
||||
return tags;
|
||||
const content = this.task.content || "";
|
||||
const results: string[] = [];
|
||||
let i = 0;
|
||||
while (i < content.length) {
|
||||
const hashIndex = content.indexOf("#", i);
|
||||
if (hashIndex === -1) break;
|
||||
// Count consecutive backslashes immediately before '#'
|
||||
let bsCount = 0;
|
||||
let j = hashIndex - 1;
|
||||
while (j >= 0 && content[j] === "\\") {
|
||||
bsCount++;
|
||||
j--;
|
||||
}
|
||||
// If odd number of backslashes precede '#', it is escaped → skip
|
||||
if (bsCount % 2 === 1) {
|
||||
i = hashIndex + 1;
|
||||
continue;
|
||||
}
|
||||
// Extract tag text: allow a-zA-Z0-9, '/', '-', '_', and non-ASCII (e.g., Chinese)
|
||||
let k = hashIndex + 1;
|
||||
while (k < content.length) {
|
||||
const ch = content[k];
|
||||
const code = ch.charCodeAt(0);
|
||||
const isAsciiAlnum =
|
||||
(code >= 48 && code <= 57) ||
|
||||
(code >= 65 && code <= 90) ||
|
||||
(code >= 97 && code <= 122);
|
||||
const isAllowed =
|
||||
isAsciiAlnum ||
|
||||
ch === "/" ||
|
||||
ch === "-" ||
|
||||
ch === "_" ||
|
||||
code > 127;
|
||||
if (!isAllowed) break;
|
||||
k++;
|
||||
}
|
||||
if (k > hashIndex + 1) {
|
||||
results.push(content.substring(hashIndex, k));
|
||||
i = k;
|
||||
} else {
|
||||
i = hashIndex + 1;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private getPriorityClass(): string {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import {
|
|||
} from "obsidian";
|
||||
import { DEFAULT_SYMBOLS, TAG_REGEX } from "../../../common/default-symbol";
|
||||
|
||||
// Use a non-global, start-anchored tag matcher to allow index checks
|
||||
const TAG_HEAD = new RegExp("^" + TAG_REGEX.source);
|
||||
|
||||
/**
|
||||
* Remove tags while protecting content inside wiki links
|
||||
*/
|
||||
|
|
@ -13,6 +16,17 @@ function removeTagsWithLinkProtection(text: string): string {
|
|||
let result = "";
|
||||
let i = 0;
|
||||
|
||||
// Helper: check if '#' at index i is escaped by odd number of backslashes
|
||||
function isEscapedHash(idx: number): boolean {
|
||||
let bs = 0;
|
||||
let j = idx - 1;
|
||||
while (j >= 0 && text[j] === "\\") {
|
||||
bs++;
|
||||
j--;
|
||||
}
|
||||
return bs % 2 === 1;
|
||||
}
|
||||
|
||||
while (i < text.length) {
|
||||
// Check if we're at the start of a wiki link
|
||||
if (i < text.length - 1 && text[i] === "[" && text[i + 1] === "[") {
|
||||
|
|
@ -38,11 +52,23 @@ function removeTagsWithLinkProtection(text: string): string {
|
|||
result += text.substring(i, linkEnd);
|
||||
i = linkEnd;
|
||||
} else if (text[i] === "#") {
|
||||
// Ignore escaped \#
|
||||
if (isEscapedHash(i)) {
|
||||
result += text[i];
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
// Check if this is a tag (not inside a link)
|
||||
const tagMatch = text.substring(i).match(TAG_REGEX);
|
||||
if (tagMatch && tagMatch.index === 0) {
|
||||
// Skip the entire tag
|
||||
i += tagMatch[0].length;
|
||||
const headMatch = TAG_HEAD.exec(text.substring(i));
|
||||
if (headMatch) {
|
||||
const full = headMatch[0];
|
||||
const body = full.slice(1);
|
||||
// Preserve only pure numeric tokens like #123 (not a tag by spec)
|
||||
if (/^\d+$/.test(body)) {
|
||||
result += full; // keep as plain text
|
||||
}
|
||||
// Otherwise treat as tag and remove it
|
||||
i += full.length;
|
||||
} else {
|
||||
// Not a tag, keep the character
|
||||
result += text[i];
|
||||
|
|
@ -240,10 +266,50 @@ export function clearAllMarks(markdown: string): string {
|
|||
tempMarkdown = tempMarkdown.replace(/\s*📁\s*/g, " ");
|
||||
|
||||
// Remove any remaining simple tags but preserve special tags like #123-123-123
|
||||
tempMarkdown = tempMarkdown.replace(
|
||||
/#(?![0-9-]+\b)[^\u2000-\u206F\u2E00-\u2E7F'!"#$%&()*+,.:;<=>?@^`{|}~\[\]\\\s]+/g,
|
||||
""
|
||||
);
|
||||
// Also ignore escaped \# (do not treat as tag)
|
||||
tempMarkdown = (function removeSimpleTagsIgnoringEscapes(
|
||||
input: string
|
||||
): string {
|
||||
let out = "";
|
||||
let i = 0;
|
||||
function isEscapedHashAt(idx: number): boolean {
|
||||
let bs = 0;
|
||||
let j = idx - 1;
|
||||
while (j >= 0 && input[j] === "\\") {
|
||||
bs++;
|
||||
j--;
|
||||
}
|
||||
return bs % 2 === 1;
|
||||
}
|
||||
while (i < input.length) {
|
||||
if (input[i] === "#") {
|
||||
if (isEscapedHashAt(i)) {
|
||||
out += "#";
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
const rest = input.substring(i);
|
||||
const m = TAG_HEAD.exec(rest);
|
||||
if (m) {
|
||||
const full = m[0];
|
||||
const body = full.slice(1);
|
||||
// Preserve only pure numeric tokens like #123; others are tags to remove
|
||||
if (/^\d+$/.test(body)) {
|
||||
out += full;
|
||||
}
|
||||
i += full.length;
|
||||
continue;
|
||||
}
|
||||
// not a tag, keep '#'
|
||||
out += "#";
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
out += input[i];
|
||||
i++;
|
||||
}
|
||||
return out;
|
||||
})(tempMarkdown);
|
||||
|
||||
// Remove any remaining tilde symbols (~ symbol) that weren't handled by the special case
|
||||
tempMarkdown = tempMarkdown.replace(/\s+~\s+/g, " ");
|
||||
|
|
|
|||
|
|
@ -158,48 +158,70 @@ export class WriteAPI {
|
|||
const previousMark = task.status || " ";
|
||||
const isCompleting = willComplete && !task.completed;
|
||||
const isAbandoning = markToWrite === "-" && previousMark !== "-";
|
||||
const isStarting = (markToWrite === ">" || markToWrite === "/") &&
|
||||
const isStarting =
|
||||
(markToWrite === ">" || markToWrite === "/") &&
|
||||
(previousMark === " " || previousMark === "?");
|
||||
|
||||
|
||||
// Add completion date if completing and not already present
|
||||
if (isCompleting) {
|
||||
const hasCompletionMeta = /(\[completion::|✅)/.test(taskLine);
|
||||
if (!hasCompletionMeta) {
|
||||
const completionDate = moment().format("YYYY-MM-DD");
|
||||
const useDataviewFormat =
|
||||
this.plugin.settings.preferMetadataFormat === "dataview";
|
||||
this.plugin.settings.preferMetadataFormat ===
|
||||
"dataview";
|
||||
const completionMeta = useDataviewFormat
|
||||
? `[completion:: ${completionDate}]`
|
||||
: `✅ ${completionDate}`;
|
||||
taskLine = this.insertDateAtCorrectPosition(taskLine, completionMeta, "completed");
|
||||
taskLine = this.insertDateAtCorrectPosition(
|
||||
taskLine,
|
||||
completionMeta,
|
||||
"completed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add cancelled date if abandoning
|
||||
if (isAbandoning && this.plugin.settings.autoDateManager?.manageCancelledDate) {
|
||||
if (
|
||||
isAbandoning &&
|
||||
this.plugin.settings.autoDateManager?.manageCancelledDate
|
||||
) {
|
||||
const hasCancelledMeta = /(\[cancelled::|❌)/.test(taskLine);
|
||||
if (!hasCancelledMeta) {
|
||||
const cancelledDate = moment().format("YYYY-MM-DD");
|
||||
const useDataviewFormat =
|
||||
this.plugin.settings.preferMetadataFormat === "dataview";
|
||||
this.plugin.settings.preferMetadataFormat ===
|
||||
"dataview";
|
||||
const cancelledMeta = useDataviewFormat
|
||||
? `[cancelled:: ${cancelledDate}]`
|
||||
: `❌ ${cancelledDate}`;
|
||||
taskLine = this.insertDateAtCorrectPosition(taskLine, cancelledMeta, "cancelled");
|
||||
taskLine = this.insertDateAtCorrectPosition(
|
||||
taskLine,
|
||||
cancelledMeta,
|
||||
"cancelled"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add start date if starting
|
||||
if (isStarting && this.plugin.settings.autoDateManager?.manageStartDate) {
|
||||
if (
|
||||
isStarting &&
|
||||
this.plugin.settings.autoDateManager?.manageStartDate
|
||||
) {
|
||||
const hasStartMeta = /(\[start::|🛫|🚀)/.test(taskLine);
|
||||
if (!hasStartMeta) {
|
||||
const startDate = moment().format("YYYY-MM-DD");
|
||||
const useDataviewFormat =
|
||||
this.plugin.settings.preferMetadataFormat === "dataview";
|
||||
this.plugin.settings.preferMetadataFormat ===
|
||||
"dataview";
|
||||
const startMeta = useDataviewFormat
|
||||
? `[start:: ${startDate}]`
|
||||
: `🛫 ${startDate}`;
|
||||
taskLine = this.insertDateAtCorrectPosition(taskLine, startMeta, "start");
|
||||
taskLine = this.insertDateAtCorrectPosition(
|
||||
taskLine,
|
||||
startMeta,
|
||||
"start"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +328,7 @@ export class WriteAPI {
|
|||
|
||||
const updatedTask = { ...originalTask, ...args.updates };
|
||||
let taskLine = lines[originalTask.line];
|
||||
|
||||
|
||||
// Track previous status for date management
|
||||
const previousStatus = originalTask.status || " ";
|
||||
let newStatus = previousStatus;
|
||||
|
|
@ -329,61 +351,88 @@ export class WriteAPI {
|
|||
`$1${statusMark}$2`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Handle date writing based on status changes
|
||||
const configuredCompleted = (
|
||||
this.plugin.settings.taskStatuses?.completed || "x"
|
||||
).split("|")[0];
|
||||
const isCompleting = (newStatus === "x" || newStatus === configuredCompleted) &&
|
||||
(previousStatus !== "x" && previousStatus !== configuredCompleted);
|
||||
const isCompleting =
|
||||
(newStatus === "x" || newStatus === configuredCompleted) &&
|
||||
previousStatus !== "x" &&
|
||||
previousStatus !== configuredCompleted;
|
||||
const isAbandoning = newStatus === "-" && previousStatus !== "-";
|
||||
const isStarting = (newStatus === ">" || newStatus === "/") &&
|
||||
const isStarting =
|
||||
(newStatus === ">" || newStatus === "/") &&
|
||||
(previousStatus === " " || previousStatus === "?");
|
||||
|
||||
|
||||
// Add completion date if completing
|
||||
if (isCompleting && !args.updates.metadata?.completedDate) {
|
||||
const hasCompletionMeta = /(\[completion::|✅)/.test(taskLine);
|
||||
if (!hasCompletionMeta) {
|
||||
const completionDate = moment().format("YYYY-MM-DD");
|
||||
const useDataviewFormat =
|
||||
this.plugin.settings.preferMetadataFormat === "dataview";
|
||||
this.plugin.settings.preferMetadataFormat ===
|
||||
"dataview";
|
||||
const completionMeta = useDataviewFormat
|
||||
? `[completion:: ${completionDate}]`
|
||||
: `✅ ${completionDate}`;
|
||||
taskLine = this.insertDateAtCorrectPosition(taskLine, completionMeta, "completed");
|
||||
taskLine = this.insertDateAtCorrectPosition(
|
||||
taskLine,
|
||||
completionMeta,
|
||||
"completed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add cancelled date if abandoning
|
||||
if (isAbandoning && this.plugin.settings.autoDateManager?.manageCancelledDate) {
|
||||
if (
|
||||
isAbandoning &&
|
||||
this.plugin.settings.autoDateManager?.manageCancelledDate
|
||||
) {
|
||||
const hasCancelledMeta = /(\[cancelled::|❌)/.test(taskLine);
|
||||
if (!hasCancelledMeta) {
|
||||
const cancelledDate = moment().format("YYYY-MM-DD");
|
||||
const useDataviewFormat =
|
||||
this.plugin.settings.preferMetadataFormat === "dataview";
|
||||
this.plugin.settings.preferMetadataFormat ===
|
||||
"dataview";
|
||||
const cancelledMeta = useDataviewFormat
|
||||
? `[cancelled:: ${cancelledDate}]`
|
||||
: `❌ ${cancelledDate}`;
|
||||
taskLine = this.insertDateAtCorrectPosition(taskLine, cancelledMeta, "cancelled");
|
||||
taskLine = this.insertDateAtCorrectPosition(
|
||||
taskLine,
|
||||
cancelledMeta,
|
||||
"cancelled"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add start date if starting
|
||||
if (isStarting && this.plugin.settings.autoDateManager?.manageStartDate) {
|
||||
if (
|
||||
isStarting &&
|
||||
this.plugin.settings.autoDateManager?.manageStartDate
|
||||
) {
|
||||
const hasStartMeta = /(\[start::|🛫|🚀)/.test(taskLine);
|
||||
if (!hasStartMeta) {
|
||||
const startDate = moment().format("YYYY-MM-DD");
|
||||
const useDataviewFormat =
|
||||
this.plugin.settings.preferMetadataFormat === "dataview";
|
||||
this.plugin.settings.preferMetadataFormat ===
|
||||
"dataview";
|
||||
const startMeta = useDataviewFormat
|
||||
? `[start:: ${startDate}]`
|
||||
: `🛫 ${startDate}`;
|
||||
taskLine = this.insertDateAtCorrectPosition(taskLine, startMeta, "start");
|
||||
taskLine = this.insertDateAtCorrectPosition(
|
||||
taskLine,
|
||||
startMeta,
|
||||
"start"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update content if changed (but prevent clearing content)
|
||||
if (args.updates.content !== undefined && args.updates.content !== "") {
|
||||
if (
|
||||
args.updates.content !== undefined &&
|
||||
args.updates.content !== ""
|
||||
) {
|
||||
// Extract the task prefix and metadata
|
||||
const prefixMatch = taskLine.match(
|
||||
/^(\s*[-*+]\s*\[[^\]]*\]\s*)/
|
||||
|
|
@ -399,7 +448,10 @@ export class WriteAPI {
|
|||
}
|
||||
} else if (args.updates.content === "") {
|
||||
// Log warning if attempting to clear content
|
||||
console.warn("[WriteAPI] Prevented clearing task content for task:", originalTask.id);
|
||||
console.warn(
|
||||
"[WriteAPI] Prevented clearing task content for task:",
|
||||
originalTask.id
|
||||
);
|
||||
}
|
||||
|
||||
// Update metadata if changed
|
||||
|
|
@ -564,10 +616,14 @@ export class WriteAPI {
|
|||
let newFilePath = originalTask.filePath;
|
||||
|
||||
// Handle content updates (i.e., renaming the file itself)
|
||||
if (updates.content !== undefined && updates.content !== originalTask.content) {
|
||||
if (
|
||||
updates.content !== undefined &&
|
||||
updates.content !== originalTask.content
|
||||
) {
|
||||
try {
|
||||
// Get effective content field settings
|
||||
const settings = this.plugin.settings.fileSource?.fileTaskProperties || {};
|
||||
const settings =
|
||||
this.plugin.settings.fileSource?.fileTaskProperties || {};
|
||||
const displayMode = settings.contentSource || "filename";
|
||||
const preferFrontmatterTitle = settings.preferFrontmatterTitle;
|
||||
const customContentField = (settings as any).customContentField;
|
||||
|
|
@ -826,10 +882,16 @@ export class WriteAPI {
|
|||
let newContent = content;
|
||||
if (args.parent) {
|
||||
// Insert as subtask
|
||||
newContent = this.insertSubtask(content, args.parent, taskContent);
|
||||
newContent = this.insertSubtask(
|
||||
content,
|
||||
args.parent,
|
||||
taskContent
|
||||
);
|
||||
} else {
|
||||
// Append to end of file
|
||||
newContent = content ? `${content}\n${taskContent}` : taskContent;
|
||||
newContent = content
|
||||
? `${content}\n${taskContent}`
|
||||
: taskContent;
|
||||
}
|
||||
|
||||
// Notify about write operation
|
||||
|
|
@ -952,7 +1014,9 @@ export class WriteAPI {
|
|||
if (!fileUpdates.has(task.filePath)) {
|
||||
fileUpdates.set(task.filePath, new Map());
|
||||
}
|
||||
fileUpdates.get(task.filePath)!.set(task.line, updatedContent);
|
||||
fileUpdates
|
||||
.get(task.filePath)!
|
||||
.set(task.line, updatedContent);
|
||||
updatedCount++;
|
||||
}
|
||||
}
|
||||
|
|
@ -981,7 +1045,9 @@ export class WriteAPI {
|
|||
const metadata = metadataMatch
|
||||
? metadataMatch[0]
|
||||
: "";
|
||||
lines[lineNum] = `${prefix}${newContent}${metadata}`;
|
||||
lines[
|
||||
lineNum
|
||||
] = `${prefix}${newContent}${metadata}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1076,7 +1142,10 @@ export class WriteAPI {
|
|||
const lineIndentLevel = lineIndentMatch
|
||||
? lineIndentMatch[0].length
|
||||
: 0;
|
||||
if (lineIndentLevel <= parentIndentLevel && line.trim() !== "") {
|
||||
if (
|
||||
lineIndentLevel <= parentIndentLevel &&
|
||||
line.trim() !== ""
|
||||
) {
|
||||
break;
|
||||
}
|
||||
insertLine++;
|
||||
|
|
@ -1103,6 +1172,105 @@ export class WriteAPI {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible: batch update task status (wrapper)
|
||||
*/
|
||||
async batchUpdateTaskStatus(args: {
|
||||
taskIds: string[];
|
||||
status?: string;
|
||||
completed?: boolean;
|
||||
}): Promise<{
|
||||
updated: string[];
|
||||
failed: Array<{ id: string; error: string }>;
|
||||
}> {
|
||||
const updated: string[] = [];
|
||||
const failed: Array<{ id: string; error: string }> = [];
|
||||
|
||||
for (const taskId of args.taskIds) {
|
||||
const result = await this.updateTaskStatus({
|
||||
taskId,
|
||||
status: args.status,
|
||||
completed: args.completed,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
updated.push(taskId);
|
||||
} else {
|
||||
failed.push({
|
||||
id: taskId,
|
||||
error: result.error || "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { updated, failed };
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible: postpone tasks to a new date (wrapper)
|
||||
*/
|
||||
async postponeTasks(args: { taskIds: string[]; newDate: string }): Promise<{
|
||||
updated: string[];
|
||||
failed: Array<{ id: string; error: string }>;
|
||||
}> {
|
||||
const updated: string[] = [];
|
||||
const failed: Array<{ id: string; error: string }> = [];
|
||||
|
||||
const parseDateOrOffset = (input: string): number | null => {
|
||||
const abs = Date.parse(input);
|
||||
if (!isNaN(abs)) return abs;
|
||||
const m = input.match(/^\+(\d+)([dwmy])$/i);
|
||||
if (!m) return null;
|
||||
const n = parseInt(m[1], 10);
|
||||
const unit = m[2].toLowerCase();
|
||||
const base = new Date();
|
||||
switch (unit) {
|
||||
case "d":
|
||||
base.setDate(base.getDate() + n);
|
||||
break;
|
||||
case "w":
|
||||
base.setDate(base.getDate() + n * 7);
|
||||
break;
|
||||
case "m":
|
||||
base.setMonth(base.getMonth() + n);
|
||||
break;
|
||||
case "y":
|
||||
base.setFullYear(base.getFullYear() + n);
|
||||
break;
|
||||
}
|
||||
return base.getTime();
|
||||
};
|
||||
|
||||
const newDateMs = parseDateOrOffset(args.newDate);
|
||||
if (newDateMs === null) {
|
||||
return {
|
||||
updated: [],
|
||||
failed: args.taskIds.map((id) => ({
|
||||
id,
|
||||
error: "Invalid date format",
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
for (const taskId of args.taskIds) {
|
||||
const result = await this.updateTask({
|
||||
taskId,
|
||||
updates: { metadata: { dueDate: newDateMs } as any },
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
updated.push(taskId);
|
||||
} else {
|
||||
failed.push({
|
||||
id: taskId,
|
||||
error: result.error || "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { updated, failed };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all descendant task IDs for a given task
|
||||
*/
|
||||
|
|
@ -1285,6 +1453,27 @@ export class WriteAPI {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible: create a task in daily note (wrapper)
|
||||
*/
|
||||
async createTaskInDailyNote(
|
||||
args: CreateTaskArgs & { heading?: string }
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
return this.addTaskToDailyNote({
|
||||
content: args.content,
|
||||
parent: args.parent,
|
||||
tags: args.tags,
|
||||
project: args.project,
|
||||
context: args.context,
|
||||
priority: args.priority,
|
||||
startDate: args.startDate,
|
||||
dueDate: args.dueDate,
|
||||
heading: (args as any).heading,
|
||||
completed: !!args.completed,
|
||||
completedDate: args.completedDate,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a project task to quick capture
|
||||
*/
|
||||
|
|
@ -1330,17 +1519,16 @@ export class WriteAPI {
|
|||
}
|
||||
|
||||
// Save to quick capture
|
||||
await saveCapture(
|
||||
this.app,
|
||||
line,
|
||||
{
|
||||
targetHeading: args.heading,
|
||||
targetFile: this.plugin.settings.quickCapture?.targetFile,
|
||||
targetType: this.plugin.settings.quickCapture?.targetType || "fixed",
|
||||
appendToFile: "append"
|
||||
}
|
||||
);
|
||||
const filePath = this.plugin.settings.quickCapture?.targetFile || "quick-capture.md"; // Use the target file
|
||||
await saveCapture(this.app, line, {
|
||||
targetHeading: args.heading,
|
||||
targetFile: this.plugin.settings.quickCapture?.targetFile,
|
||||
targetType:
|
||||
this.plugin.settings.quickCapture?.targetType || "fixed",
|
||||
appendToFile: "append",
|
||||
});
|
||||
const filePath =
|
||||
this.plugin.settings.quickCapture?.targetFile ||
|
||||
"quick-capture.md"; // Use the target file
|
||||
|
||||
// Notify about write operation
|
||||
emit(this.app, Events.WRITE_OPERATION_START, { path: filePath });
|
||||
|
|
@ -1625,11 +1813,7 @@ export class WriteAPI {
|
|||
/**
|
||||
* Add interval to date based on unit
|
||||
*/
|
||||
private addInterval(
|
||||
base: Date,
|
||||
interval: number,
|
||||
unit: string
|
||||
): number {
|
||||
private addInterval(base: Date, interval: number, unit: string): number {
|
||||
const n = interval;
|
||||
switch (unit) {
|
||||
case "d":
|
||||
|
|
@ -1893,29 +2077,44 @@ export class WriteAPI {
|
|||
// Check for block reference at the end
|
||||
const blockRefPattern = /\s*(\^[a-zA-Z0-9-]+)$/;
|
||||
const blockRefMatch = taskLine.match(blockRefPattern);
|
||||
|
||||
|
||||
if (blockRefMatch && blockRefMatch.index !== undefined) {
|
||||
// Insert before block reference
|
||||
const insertPos = blockRefMatch.index;
|
||||
return taskLine.slice(0, insertPos) + " " + dateMetadata + taskLine.slice(insertPos);
|
||||
return (
|
||||
taskLine.slice(0, insertPos) +
|
||||
" " +
|
||||
dateMetadata +
|
||||
taskLine.slice(insertPos)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// For completion date, add at the very end
|
||||
if (dateType === "completed") {
|
||||
return taskLine + " " + dateMetadata;
|
||||
}
|
||||
|
||||
|
||||
// For cancelled and start dates, insert after task content but before other metadata
|
||||
// Find where metadata starts (tags, dates, etc)
|
||||
const metadataPattern = /([\s]+(🔺|⏫|🔼|🔽|⏬|🛫|⏳|📅|✅|🔁|\[[\w]+::|#|@|\+).*)?$/;
|
||||
const metadataPattern =
|
||||
/([\s]+(🔺|⏫|🔼|🔽|⏬|🛫|⏳|📅|✅|🔁|\[[\w]+::|#|@|\+).*)?$/;
|
||||
const metadataMatch = taskLine.match(metadataPattern);
|
||||
|
||||
if (metadataMatch && metadataMatch.index !== undefined && metadataMatch[0].trim()) {
|
||||
|
||||
if (
|
||||
metadataMatch &&
|
||||
metadataMatch.index !== undefined &&
|
||||
metadataMatch[0].trim()
|
||||
) {
|
||||
// Insert before existing metadata
|
||||
const insertPos = metadataMatch.index;
|
||||
return taskLine.slice(0, insertPos) + " " + dateMetadata + taskLine.slice(insertPos);
|
||||
return (
|
||||
taskLine.slice(0, insertPos) +
|
||||
" " +
|
||||
dateMetadata +
|
||||
taskLine.slice(insertPos)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// No metadata found, add at the end
|
||||
return taskLine + " " + dateMetadata;
|
||||
}
|
||||
|
|
@ -2092,10 +2291,10 @@ export class WriteAPI {
|
|||
if (unit.endsWith("s")) {
|
||||
unit = unit.substring(0, unit.length - 1);
|
||||
}
|
||||
|
||||
|
||||
// Start from base date
|
||||
let nextDate = new Date(baseDate);
|
||||
|
||||
|
||||
// Keep advancing the date until it's in the future
|
||||
while (nextDate.getTime() <= todayStart.getTime()) {
|
||||
switch (unit) {
|
||||
|
|
@ -2103,19 +2302,25 @@ export class WriteAPI {
|
|||
nextDate.setDate(nextDate.getDate() + interval);
|
||||
break;
|
||||
case "week":
|
||||
nextDate.setDate(nextDate.getDate() + interval * 7);
|
||||
nextDate.setDate(
|
||||
nextDate.getDate() + interval * 7
|
||||
);
|
||||
break;
|
||||
case "month":
|
||||
// Save the original day of month for proper month rolling
|
||||
const originalDay = baseDate.getDate();
|
||||
nextDate.setMonth(nextDate.getMonth() + interval);
|
||||
nextDate.setMonth(
|
||||
nextDate.getMonth() + interval
|
||||
);
|
||||
// If day has changed (e.g., Jan 31 -> Feb 28), adjust back
|
||||
if (nextDate.getDate() !== originalDay) {
|
||||
nextDate.setDate(0); // Go to last day of previous month
|
||||
}
|
||||
break;
|
||||
case "year":
|
||||
nextDate.setFullYear(nextDate.getFullYear() + interval);
|
||||
nextDate.setFullYear(
|
||||
nextDate.getFullYear() + interval
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// Default to days if unit is not recognized
|
||||
|
|
@ -2123,10 +2328,10 @@ export class WriteAPI {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Normalize to midnight
|
||||
nextDate.setHours(0, 0, 0, 0);
|
||||
|
||||
|
||||
// Convert to UTC noon timestamp for consistent storage
|
||||
const year = nextDate.getFullYear();
|
||||
const month = nextDate.getMonth();
|
||||
|
|
@ -2140,9 +2345,9 @@ export class WriteAPI {
|
|||
if (simpleMatch) {
|
||||
const interval = parseInt(simpleMatch[1]);
|
||||
const unit = simpleMatch[2];
|
||||
|
||||
|
||||
let nextDate = new Date(baseDate);
|
||||
|
||||
|
||||
// Keep advancing the date until it's in the future
|
||||
while (nextDate.getTime() <= todayStart.getTime()) {
|
||||
switch (unit) {
|
||||
|
|
@ -2161,14 +2366,16 @@ export class WriteAPI {
|
|||
}
|
||||
break;
|
||||
case "y":
|
||||
nextDate.setFullYear(nextDate.getFullYear() + interval);
|
||||
nextDate.setFullYear(
|
||||
nextDate.getFullYear() + interval
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Normalize to midnight
|
||||
nextDate.setHours(0, 0, 0, 0);
|
||||
|
||||
|
||||
// Convert to UTC noon timestamp
|
||||
const year = nextDate.getFullYear();
|
||||
const month = nextDate.getMonth();
|
||||
|
|
@ -2183,7 +2390,6 @@ export class WriteAPI {
|
|||
const month = tomorrow.getMonth();
|
||||
const day = tomorrow.getDate();
|
||||
return Date.UTC(year, month, day, 12, 0, 0);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error calculating next due date:", error);
|
||||
// Return tomorrow as fallback
|
||||
|
|
@ -2196,4 +2402,4 @@ export class WriteAPI {
|
|||
return Date.UTC(year, month, day, 12, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -301,7 +301,8 @@ export class MarkdownTaskParser {
|
|||
private extractTaskLine(
|
||||
line: string
|
||||
): [number, number, string, string] | null {
|
||||
const trimmed = line.trim();
|
||||
// Preserve trailing spaces to allow parsing of empty-content tasks like "- [ ] "
|
||||
const trimmed = line.trimStart();
|
||||
const actualSpaces = line.length - trimmed.length;
|
||||
|
||||
if (this.isTaskLine(trimmed)) {
|
||||
|
|
@ -772,8 +773,6 @@ export class MarkdownTaskParser {
|
|||
|
||||
if (key && value) {
|
||||
// Debug: Log dataview metadata extraction for configured prefixes
|
||||
|
||||
|
||||
|
||||
const before = content.substring(0, start);
|
||||
const after = content.substring(end + 1);
|
||||
|
|
@ -944,85 +943,82 @@ export class MarkdownTaskParser {
|
|||
const detector = new ContextDetector(content);
|
||||
detector.detectAllProtectedRanges();
|
||||
|
||||
const hashPos = detector.findNextUnprotectedHash(0);
|
||||
if (hashPos === -1) return null;
|
||||
const tryFrom = (startPos: number): [string, string, string] | null => {
|
||||
const hashPos = detector.findNextUnprotectedHash(startPos);
|
||||
if (hashPos === -1) return null;
|
||||
|
||||
// Enhanced word boundary check
|
||||
const isWordStart = this.isValidTagStart(content, hashPos);
|
||||
if (!isWordStart) {
|
||||
// Try to find the next unprotected hash
|
||||
const nextHashPos = detector.findNextUnprotectedHash(hashPos + 1);
|
||||
if (nextHashPos === -1) return null;
|
||||
|
||||
// Recursively check the remaining content
|
||||
const remainingContent = content.substring(nextHashPos);
|
||||
const recurseResult = this.extractTag(remainingContent);
|
||||
if (recurseResult) {
|
||||
const [tag, beforeTag, afterTag] = recurseResult;
|
||||
return [
|
||||
tag,
|
||||
content.substring(0, nextHashPos) + beforeTag,
|
||||
afterTag,
|
||||
];
|
||||
// If an odd number of backslashes immediately precede '#', it's escaped → skip
|
||||
let bsCount = 0;
|
||||
let j = hashPos - 1;
|
||||
while (j >= 0 && content[j] === "\\") {
|
||||
bsCount++;
|
||||
j--;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const afterHash = content.substring(hashPos + 1);
|
||||
let tagEnd = 0;
|
||||
|
||||
// Find tag end, including '/' for special tags and Unicode characters
|
||||
for (let i = 0; i < afterHash.length; i++) {
|
||||
const char = afterHash[i];
|
||||
const charCode = char.charCodeAt(0);
|
||||
|
||||
// Check if character is valid for tags:
|
||||
// - ASCII letters and numbers: a-z, A-Z, 0-9
|
||||
// - Special characters: /, -, _
|
||||
// - Unicode characters (including Chinese): > 127
|
||||
// - Exclude common separators and punctuation
|
||||
if (
|
||||
(charCode >= 48 && charCode <= 57) || // 0-9
|
||||
(charCode >= 65 && charCode <= 90) || // A-Z
|
||||
(charCode >= 97 && charCode <= 122) || // a-z
|
||||
char === "/" ||
|
||||
char === "-" ||
|
||||
char === "_" ||
|
||||
(charCode > 127 &&
|
||||
char !== "," &&
|
||||
char !== "。" &&
|
||||
char !== ";" &&
|
||||
char !== ":" &&
|
||||
char !== "!" &&
|
||||
char !== "?" &&
|
||||
char !== "「" &&
|
||||
char !== "」" &&
|
||||
char !== "『" &&
|
||||
char !== "』" &&
|
||||
char !== "(" &&
|
||||
char !== ")" &&
|
||||
char !== "【" &&
|
||||
char !== "】" &&
|
||||
char !== '"' &&
|
||||
char !== '"' &&
|
||||
char !== "'" &&
|
||||
char !== "'" &&
|
||||
char !== " ")
|
||||
) {
|
||||
tagEnd = i + 1;
|
||||
} else {
|
||||
break;
|
||||
if (bsCount % 2 === 1) {
|
||||
return tryFrom(hashPos + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (tagEnd > 0) {
|
||||
const fullTag = "#" + afterHash.substring(0, tagEnd); // Include # prefix
|
||||
const before = content.substring(0, hashPos);
|
||||
const after = content.substring(hashPos + 1 + tagEnd);
|
||||
return [fullTag, before, after];
|
||||
}
|
||||
// Enhanced word boundary check
|
||||
const isWordStart = this.isValidTagStart(content, hashPos);
|
||||
if (!isWordStart) {
|
||||
return tryFrom(hashPos + 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
const afterHash = content.substring(hashPos + 1);
|
||||
let tagEnd = 0;
|
||||
|
||||
// Find tag end, including '/' for special tags and Unicode characters
|
||||
for (let i = 0; i < afterHash.length; i++) {
|
||||
const char = afterHash[i];
|
||||
const charCode = char.charCodeAt(0);
|
||||
|
||||
// Valid tag characters
|
||||
if (
|
||||
(charCode >= 48 && charCode <= 57) || // 0-9
|
||||
(charCode >= 65 && charCode <= 90) || // A-Z
|
||||
(charCode >= 97 && charCode <= 122) || // a-z
|
||||
char === "/" ||
|
||||
char === "-" ||
|
||||
char === "_" ||
|
||||
(charCode > 127 &&
|
||||
char !== "," &&
|
||||
char !== "。" &&
|
||||
char !== ";" &&
|
||||
char !== ":" &&
|
||||
char !== "!" &&
|
||||
char !== "?" &&
|
||||
char !== "「" &&
|
||||
char !== "」" &&
|
||||
char !== "『" &&
|
||||
char !== "』" &&
|
||||
char !== "(" &&
|
||||
char !== ")" &&
|
||||
char !== "【" &&
|
||||
char !== "】" &&
|
||||
char !== '"' &&
|
||||
char !== '"' &&
|
||||
char !== "'" &&
|
||||
char !== "'" &&
|
||||
char !== " ")
|
||||
) {
|
||||
tagEnd = i + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tagEnd > 0) {
|
||||
const fullTag = "#" + afterHash.substring(0, tagEnd); // Include # prefix
|
||||
const before = content.substring(0, hashPos);
|
||||
const after = content.substring(hashPos + 1 + tagEnd);
|
||||
return [fullTag, before, after];
|
||||
}
|
||||
|
||||
// Not a valid tag, continue searching
|
||||
return tryFrom(hashPos + 1);
|
||||
};
|
||||
|
||||
return tryFrom(0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1763,9 +1759,9 @@ export class MarkdownTaskParser {
|
|||
// Early return if enhanced project features are disabled
|
||||
// Check if file metadata inheritance is enabled
|
||||
if (!this.config.fileMetadataInheritance?.enabled) {
|
||||
// Parser should not perform file-level inheritance when disabled
|
||||
// Leave any file/frontmatter merging to Augmentor when enabled
|
||||
return {};
|
||||
// Inheritance disabled: preserve task-level metadata as-is
|
||||
// (enhanced merging will be handled elsewhere when enabled)
|
||||
return inherited;
|
||||
}
|
||||
|
||||
// Check if frontmatter inheritance is enabled
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ function handleAutoDateManagerTransaction(
|
|||
return tr;
|
||||
}
|
||||
|
||||
const {doc, lineNumber, oldStatus, newStatus} = taskStatusChangeInfo;
|
||||
const { doc, lineNumber, oldStatus, newStatus } = taskStatusChangeInfo;
|
||||
|
||||
// Determine what date operations need to be performed
|
||||
const dateOperations = determineDateOperations(
|
||||
|
|
@ -463,7 +463,9 @@ function applyDateOperations(
|
|||
let lineText = line.text;
|
||||
const changes = [];
|
||||
|
||||
console.log(`[AutoDateManager] applyDateOperations - Working with line: "${lineText}"`);
|
||||
console.log(
|
||||
`[AutoDateManager] applyDateOperations - Working with line: "${lineText}"`
|
||||
);
|
||||
|
||||
for (const operation of operations) {
|
||||
if (operation.type === "add") {
|
||||
|
|
@ -499,12 +501,18 @@ function applyDateOperations(
|
|||
|
||||
const absolutePosition = line.from + insertPosition;
|
||||
|
||||
console.log(`[AutoDateManager] Inserting ${operation.dateType} date:`);
|
||||
console.log(
|
||||
`[AutoDateManager] Inserting ${operation.dateType} date:`
|
||||
);
|
||||
console.log(` - Insert position (relative): ${insertPosition}`);
|
||||
console.log(` - Line.from: ${line.from}`);
|
||||
console.log(` - Absolute position: ${absolutePosition}`);
|
||||
console.log(` - Date text: "${dateText}"`);
|
||||
console.log(` - Text at insert point: "${lineText.substring(insertPosition)}"`);
|
||||
console.log(
|
||||
` - Text at insert point: "${lineText.substring(
|
||||
insertPosition
|
||||
)}"`
|
||||
);
|
||||
|
||||
changes.push({
|
||||
from: absolutePosition,
|
||||
|
|
@ -529,8 +537,8 @@ function applyDateOperations(
|
|||
operation.dateType === "completed"
|
||||
? "completion"
|
||||
: operation.dateType === "cancelled"
|
||||
? "cancelled"
|
||||
: "unknown";
|
||||
? "cancelled"
|
||||
: "unknown";
|
||||
datePattern = new RegExp(
|
||||
`\\s*\\[${fieldName}::\\s*\\d{4}-\\d{2}-\\d{2}(?:\\s+\\d{2}:\\d{2}(?::\\d{2})?)?\\]`,
|
||||
"g"
|
||||
|
|
@ -671,7 +679,8 @@ function findMetadataInsertPosition(
|
|||
|
||||
// For cancelled date, we need special handling to insert after all metadata and start dates
|
||||
if (dateType === "cancelled") {
|
||||
const useDataviewFormat = plugin.settings.preferMetadataFormat === "dataview";
|
||||
const useDataviewFormat =
|
||||
plugin.settings.preferMetadataFormat === "dataview";
|
||||
|
||||
// Find the last occurrence of either:
|
||||
// 1. Start date marker (🛫 or [start::)
|
||||
|
|
@ -700,11 +709,15 @@ function findMetadataInsertPosition(
|
|||
const commonStartEmojis = ["🚀", "🛫", "▶️", "⏰", "🏁"];
|
||||
for (const emoji of commonStartEmojis) {
|
||||
const pattern = new RegExp(
|
||||
`${escapeRegex(emoji)}\\s*\\d{4}-\\d{2}-\\d{2}(?:\\s+\\d{2}:\\d{2}(?::\\d{2})?)?`
|
||||
`${escapeRegex(
|
||||
emoji
|
||||
)}\\s*\\d{4}-\\d{2}-\\d{2}(?:\\s+\\d{2}:\\d{2}(?::\\d{2})?)?`
|
||||
);
|
||||
startDateMatch = lineText.match(pattern);
|
||||
if (startDateMatch) {
|
||||
console.log(`[AutoDateManager] Found start date with emoji ${emoji}`);
|
||||
console.log(
|
||||
`[AutoDateManager] Found start date with emoji ${emoji}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -713,11 +726,15 @@ function findMetadataInsertPosition(
|
|||
if (startDateMatch && startDateMatch.index !== undefined) {
|
||||
position = startDateMatch.index + startDateMatch[0].length;
|
||||
startDateFound = true;
|
||||
console.log(`[AutoDateManager] Found start date at index ${startDateMatch.index}, length ${startDateMatch[0].length}, new position: ${position}`);
|
||||
console.log(
|
||||
`[AutoDateManager] Found start date at index ${startDateMatch.index}, length ${startDateMatch[0].length}, new position: ${position}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[AutoDateManager] Start date found: ${startDateFound}, position: ${position}`);
|
||||
console.log(
|
||||
`[AutoDateManager] Start date found: ${startDateFound}, position: ${position}`
|
||||
);
|
||||
|
||||
if (!startDateFound) {
|
||||
// No start date found, find the end of all metadata
|
||||
|
|
@ -728,7 +745,7 @@ function findMetadataInsertPosition(
|
|||
const metadataRegexes = [
|
||||
/#[\w-]+/g, // Tags
|
||||
/\[[a-zA-Z]+::[^\]]*\]/g, // Dataview fields
|
||||
/[📅🚀✅❌🛫▶️⏰🏁]\s*\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2}(?::\d{2})?)?/g // Date markers (including all common start emojis)
|
||||
/[📅🚀✅❌🛫▶️⏰🏁]\s*\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2}(?::\d{2})?)?/g, // Date markers (including all common start emojis)
|
||||
];
|
||||
|
||||
for (const regex of metadataRegexes) {
|
||||
|
|
@ -747,7 +764,7 @@ function findMetadataInsertPosition(
|
|||
}
|
||||
|
||||
// Ensure we have a space before the cancelled date
|
||||
if (position > 0 && lineText[position - 1] !== ' ') {
|
||||
if (position > 0 && lineText[position - 1] !== " ") {
|
||||
// No need to add space here, it will be added with the date
|
||||
}
|
||||
} else if (dateType === "completed") {
|
||||
|
|
@ -759,7 +776,7 @@ function findMetadataInsertPosition(
|
|||
if (blockRef) {
|
||||
position = blockRef.index;
|
||||
// Remove trailing space if exists
|
||||
if (position > 0 && lineText[position - 1] === ' ') {
|
||||
if (position > 0 && lineText[position - 1] === " ") {
|
||||
position--;
|
||||
}
|
||||
}
|
||||
|
|
@ -767,13 +784,13 @@ function findMetadataInsertPosition(
|
|||
// For start date, find the end of main content before metadata
|
||||
let contentEnd = position;
|
||||
let inBrackets = false;
|
||||
const chars = lineText.slice(position).split('');
|
||||
const chars = lineText.slice(position).split("");
|
||||
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
const char = chars[i];
|
||||
|
||||
// Track if we're inside dataview brackets
|
||||
if (char === '[' && !inBrackets) {
|
||||
if (char === "[" && !inBrackets) {
|
||||
// Check if this is a dataview field like [due::...]
|
||||
const remainingText = lineText.slice(position + i);
|
||||
if (remainingText.match(/^\[[a-zA-Z]+::/)) {
|
||||
|
|
@ -782,12 +799,22 @@ function findMetadataInsertPosition(
|
|||
}
|
||||
inBrackets = true;
|
||||
contentEnd = position + i + 1;
|
||||
} else if (char === ']' && inBrackets) {
|
||||
} else if (char === "]" && inBrackets) {
|
||||
inBrackets = false;
|
||||
contentEnd = position + i + 1;
|
||||
} else if (!inBrackets) {
|
||||
// Check for metadata markers when not inside brackets
|
||||
if (char === '#' || char === '📅' || char === '🚀' || char === '✅' || char === '❌' || char === '🛫' || char === '▶️' || char === '⏰' || char === '🏁') {
|
||||
if (
|
||||
char === "#" ||
|
||||
char === "📅" ||
|
||||
char === "🚀" ||
|
||||
char === "✅" ||
|
||||
char === "❌" ||
|
||||
char === "🛫" ||
|
||||
char === "▶️" ||
|
||||
char === "⏰" ||
|
||||
char === "🏁"
|
||||
) {
|
||||
// This is metadata, stop here
|
||||
break;
|
||||
}
|
||||
|
|
@ -801,7 +828,7 @@ function findMetadataInsertPosition(
|
|||
position = contentEnd;
|
||||
|
||||
// Trim any trailing whitespace from position
|
||||
while (position > 0 && lineText[position - 1] === ' ') {
|
||||
while (position > 0 && lineText[position - 1] === " ") {
|
||||
position--;
|
||||
}
|
||||
}
|
||||
|
|
@ -810,12 +837,14 @@ function findMetadataInsertPosition(
|
|||
if (blockRef && position > blockRef.index) {
|
||||
position = blockRef.index;
|
||||
// Remove trailing space if it exists
|
||||
if (position > 0 && lineText[position - 1] === ' ') {
|
||||
if (position > 0 && lineText[position - 1] === " ") {
|
||||
position--;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[AutoDateManager] Final insert position for ${dateType}: ${position}`);
|
||||
console.log(
|
||||
`[AutoDateManager] Final insert position for ${dateType}: ${position}`
|
||||
);
|
||||
return position;
|
||||
}
|
||||
|
||||
|
|
@ -835,7 +864,7 @@ function findCompletedDateInsertPosition(
|
|||
// Insert before the block reference ID
|
||||
// Remove trailing space if exists
|
||||
let position = blockRef.index;
|
||||
if (position > 0 && lineText[position - 1] === ' ') {
|
||||
if (position > 0 && lineText[position - 1] === " ") {
|
||||
position--;
|
||||
}
|
||||
return position;
|
||||
|
|
@ -870,7 +899,7 @@ function detectBlockReference(text: string): {
|
|||
// - Can have optional whitespace before and after
|
||||
// - Block ID can contain letters, numbers, hyphens, and underscores
|
||||
// - Must be at the end of the line
|
||||
const blockRefPattern = /\s*(\^[a-zA-Z0-9-]+)$/;
|
||||
const blockRefPattern = /\s*(\^[A-Za-z0-9_-]+)\s*$/;
|
||||
const match = text.match(blockRefPattern);
|
||||
|
||||
if (match && match.index !== undefined) {
|
||||
|
|
@ -878,7 +907,7 @@ function detectBlockReference(text: string): {
|
|||
blockId: match[1],
|
||||
index: match.index,
|
||||
length: match[0].length,
|
||||
fullMatch: match[0]
|
||||
fullMatch: match[0],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -898,10 +927,10 @@ function extractBlockReference(text: string): {
|
|||
|
||||
if (blockRef) {
|
||||
const cleanedText = text.substring(0, blockRef.index).trimEnd();
|
||||
return {cleanedText, blockRef};
|
||||
return { cleanedText, blockRef };
|
||||
}
|
||||
|
||||
return {cleanedText: text, blockRef: null};
|
||||
return { cleanedText: text, blockRef: null };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -66,34 +66,28 @@ function isValidTaskMarkerReplacement(
|
|||
return false;
|
||||
}
|
||||
|
||||
// Check if user actively selected text before replacement
|
||||
const startSelection = tr.startState.selection.main;
|
||||
const hasUserSelection = startSelection && !startSelection.empty;
|
||||
|
||||
// If user had a selection that covers the replacement range, this is intentional replacement
|
||||
if (hasUserSelection && startSelection && fromA >= startSelection.from && toA <= startSelection.to) {
|
||||
console.log(
|
||||
`User selection detected (${startSelection.from}-${startSelection.to}) covering replacement range (${fromA}-${toA}). Skipping automatic cycling as this is user-intended replacement.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get valid task status marks from plugin settings
|
||||
const {marks} = getTaskStatusConfig(plugin);
|
||||
const { marks } = getTaskStatusConfig(plugin);
|
||||
const validMarks = Object.values(marks);
|
||||
|
||||
// Check if both the original and inserted characters are valid task status marks
|
||||
const isOriginalValidMark = validMarks.includes(originalText) || originalText === ' ';
|
||||
const isInsertedValidMark = validMarks.includes(insertedText) || insertedText === ' ';
|
||||
const isOriginalValidMark =
|
||||
validMarks.includes(originalText) || originalText === " ";
|
||||
const isInsertedValidMark =
|
||||
validMarks.includes(insertedText) || insertedText === " ";
|
||||
|
||||
// If either character is not a valid task mark, this is likely manual input
|
||||
if (!isOriginalValidMark || !isInsertedValidMark) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// IMPORTANT: Prevent triggering when typing regular letters in an empty checkbox
|
||||
// If original is space and inserted is a letter (not a status mark), it's typing
|
||||
if (originalText === ' ' && !validMarks.includes(insertedText) && insertedText !== ' ') {
|
||||
if (
|
||||
originalText === " " &&
|
||||
!validMarks.includes(insertedText) &&
|
||||
insertedText !== " "
|
||||
) {
|
||||
// User is typing in an empty checkbox, not changing status
|
||||
return false;
|
||||
}
|
||||
|
|
@ -181,9 +175,9 @@ export function findTaskStatusChanges(
|
|||
change.text === " " ||
|
||||
(change.text === "" &&
|
||||
(tr.startState.doc.sliceString(
|
||||
change.fromA,
|
||||
change.toA
|
||||
) === "\t" ||
|
||||
change.fromA,
|
||||
change.toA
|
||||
) === "\t" ||
|
||||
tr.startState.doc.sliceString(
|
||||
change.fromA,
|
||||
change.toA
|
||||
|
|
@ -331,29 +325,40 @@ export function findTaskStatusChanges(
|
|||
// Check if our insertion point is at the mark position
|
||||
const markIndex = newLineText.indexOf("[") + 1;
|
||||
// Don't trigger when typing the "[" character itself, only when editing the status mark within brackets
|
||||
// Also don't trigger when typing regular letters in empty checkbox
|
||||
// Also don't trigger when typing regular letters in empty checkbox (unless it's a valid status mark like x, /, etc.)
|
||||
if (
|
||||
pos === newLine.from + markIndex &&
|
||||
insertedText !== "[" &&
|
||||
// Don't trigger for regular letters typed in empty checkbox (space status)
|
||||
!(match[2] === ' ' && /[a-zA-Z]/.test(insertedText))
|
||||
!(
|
||||
match[2] === " " &&
|
||||
/[a-zA-Z]/.test(insertedText) &&
|
||||
(plugin
|
||||
? !Object.values(
|
||||
getTaskStatusConfig(plugin).marks
|
||||
).includes(insertedText)
|
||||
: true)
|
||||
)
|
||||
) {
|
||||
// Check if this is a replacement operation and validate if it's a valid task marker replacement
|
||||
if (fromA !== toA) {
|
||||
const originalText = tr.startState.doc.sliceString(fromA, toA);
|
||||
const originalText = tr.startState.doc.sliceString(
|
||||
fromA,
|
||||
toA
|
||||
);
|
||||
|
||||
// Only perform validation if plugin is provided
|
||||
if (plugin) {
|
||||
const isValidReplacement = isValidTaskMarkerReplacement(
|
||||
tr,
|
||||
fromA,
|
||||
toA,
|
||||
insertedText,
|
||||
originalText,
|
||||
pos,
|
||||
newLineText,
|
||||
plugin
|
||||
);
|
||||
const isValidReplacement =
|
||||
isValidTaskMarkerReplacement(
|
||||
tr,
|
||||
fromA,
|
||||
toA,
|
||||
insertedText,
|
||||
originalText,
|
||||
pos,
|
||||
newLineText,
|
||||
plugin
|
||||
);
|
||||
|
||||
if (!isValidReplacement) {
|
||||
console.log(
|
||||
|
|
@ -423,13 +428,13 @@ export function findTaskStatusChanges(
|
|||
wasCompleteTask: wasCompleteTask,
|
||||
tasksInfo: triggerByTasks
|
||||
? {
|
||||
isTaskChange: triggerByTasks,
|
||||
originalFromA: fromA,
|
||||
originalToA: toA,
|
||||
originalFromB: fromB,
|
||||
originalToB: toB,
|
||||
originalInsertedText: insertedText,
|
||||
}
|
||||
isTaskChange: triggerByTasks,
|
||||
originalFromA: fromA,
|
||||
originalToA: toA,
|
||||
originalFromB: fromB,
|
||||
originalToB: toB,
|
||||
originalInsertedText: insertedText,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
|
@ -531,13 +536,17 @@ export function handleCycleCompleteStatusTransaction(
|
|||
}
|
||||
|
||||
// Check if any task statuses were changed in this transaction
|
||||
const taskStatusChanges = findTaskStatusChanges(tr, !!getTasksAPI(plugin), plugin);
|
||||
const taskStatusChanges = findTaskStatusChanges(
|
||||
tr,
|
||||
!!getTasksAPI(plugin),
|
||||
plugin
|
||||
);
|
||||
if (taskStatusChanges.length === 0) {
|
||||
return tr;
|
||||
}
|
||||
|
||||
// Get the task cycle and marks from plugin settings
|
||||
const {cycle, marks, excludeMarksFromCycle} = getTaskStatusConfig(plugin);
|
||||
const { cycle, marks, excludeMarksFromCycle } = getTaskStatusConfig(plugin);
|
||||
const remainingCycle = cycle.filter(
|
||||
(state) => !excludeMarksFromCycle.includes(state)
|
||||
);
|
||||
|
|
@ -671,7 +680,7 @@ export function handleCycleCompleteStatusTransaction(
|
|||
|
||||
// Process each task status change
|
||||
for (const taskStatusInfo of taskStatusChanges) {
|
||||
const {position, currentMark, wasCompleteTask, tasksInfo} =
|
||||
const { position, currentMark, wasCompleteTask, tasksInfo } =
|
||||
taskStatusInfo;
|
||||
|
||||
if (tasksInfo?.isTaskChange) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue