mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat: support sort command for current file
This commit is contained in:
parent
0f4667a13f
commit
7b35480be0
52 changed files with 1674 additions and 1130 deletions
|
|
@ -29,8 +29,8 @@
|
|||
"devDependencies": {
|
||||
"@codemirror/language": "https://github.com/lishid/cm-language",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.36.7",
|
||||
"@datastructures-js/queue": "^4.2.3",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/node": "^16.11.6",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ importers:
|
|||
version: 1.10.0
|
||||
obsidian-daily-notes-interface:
|
||||
specifier: ^0.9.4
|
||||
version: 0.9.4(@codemirror/state@6.5.2)(@codemirror/view@6.36.5)
|
||||
version: 0.9.4(@codemirror/state@6.5.2)(@codemirror/view@6.36.7)
|
||||
devDependencies:
|
||||
'@codemirror/language':
|
||||
specifier: https://github.com/lishid/cm-language
|
||||
|
|
@ -25,11 +25,11 @@ importers:
|
|||
specifier: ^6.0.0
|
||||
version: 6.5.10
|
||||
'@codemirror/state':
|
||||
specifier: ^6.0.0
|
||||
specifier: ^6.5.2
|
||||
version: 6.5.2
|
||||
'@codemirror/view':
|
||||
specifier: ^6.0.0
|
||||
version: 6.36.5
|
||||
specifier: ^6.36.7
|
||||
version: 6.36.7
|
||||
'@datastructures-js/queue':
|
||||
specifier: ^4.2.3
|
||||
version: 4.2.3
|
||||
|
|
@ -71,7 +71,7 @@ importers:
|
|||
version: 3.0.0
|
||||
obsidian:
|
||||
specifier: ^1.8.7
|
||||
version: 1.8.7(@codemirror/state@6.5.2)(@codemirror/view@6.36.5)
|
||||
version: 1.8.7(@codemirror/state@6.5.2)(@codemirror/view@6.36.7)
|
||||
regexp-match-indices:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
|
|
@ -277,8 +277,8 @@ packages:
|
|||
'@codemirror/state@6.5.2':
|
||||
resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==}
|
||||
|
||||
'@codemirror/view@6.36.5':
|
||||
resolution: {integrity: sha512-cd+FZEUlu3GQCYnguYm3EkhJ8KJVisqqUsCOKedBoAt/d9c76JUUap6U0UrpElln5k6VyrEOYliMuDAKIeDQLg==}
|
||||
'@codemirror/view@6.36.7':
|
||||
resolution: {integrity: sha512-kCWGW/chWGPgZqfZ36Um9Iz0X2IVpmCjg1P/qY6B6a2ecXtWRRAigmpJ6YgUQ5lTWXMyyVdfmpzhLZmsZQMbtg==}
|
||||
|
||||
'@datastructures-js/queue@4.2.3':
|
||||
resolution: {integrity: sha512-GWVMorC/xi2V2ta+Z/CPgPGHL2ZJozcj48g7y2nIX5GIGZGRrbShSHgvMViJwHJurUzJYOdIdRZnWDRrROFwJA==}
|
||||
|
|
@ -289,6 +289,12 @@ packages:
|
|||
peerDependencies:
|
||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||
|
||||
'@eslint-community/eslint-utils@4.7.0':
|
||||
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||
|
||||
'@eslint-community/regexpp@4.12.1':
|
||||
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
|
||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||
|
|
@ -2263,20 +2269,20 @@ snapshots:
|
|||
dependencies:
|
||||
'@codemirror/language': 6.10.8
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.36.5
|
||||
'@codemirror/view': 6.36.7
|
||||
'@lezer/common': 1.2.3
|
||||
|
||||
'@codemirror/commands@6.8.1':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.10.8
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.36.5
|
||||
'@codemirror/view': 6.36.7
|
||||
'@lezer/common': 1.2.3
|
||||
|
||||
'@codemirror/language@6.10.8':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.36.5
|
||||
'@codemirror/view': 6.36.7
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/highlight': 1.2.1
|
||||
'@lezer/lr': 1.4.2
|
||||
|
|
@ -2285,7 +2291,7 @@ snapshots:
|
|||
'@codemirror/language@https://codeload.github.com/lishid/cm-language/tar.gz/6c1c5f5b677f6f6503d1ca2ec47f62f6406cda67':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.36.5
|
||||
'@codemirror/view': 6.36.7
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/highlight': 1.2.1
|
||||
'@lezer/lr': 1.4.2
|
||||
|
|
@ -2294,20 +2300,20 @@ snapshots:
|
|||
'@codemirror/lint@6.8.5':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.36.5
|
||||
'@codemirror/view': 6.36.7
|
||||
crelt: 1.0.6
|
||||
|
||||
'@codemirror/search@6.5.10':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.36.5
|
||||
'@codemirror/view': 6.36.7
|
||||
crelt: 1.0.6
|
||||
|
||||
'@codemirror/state@6.5.2':
|
||||
dependencies:
|
||||
'@marijn/find-cluster-break': 1.0.2
|
||||
|
||||
'@codemirror/view@6.36.5':
|
||||
'@codemirror/view@6.36.7':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
style-mod: 4.1.2
|
||||
|
|
@ -2320,6 +2326,11 @@ snapshots:
|
|||
eslint: 8.57.1
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)':
|
||||
dependencies:
|
||||
eslint: 8.57.1
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/regexpp@4.12.1': {}
|
||||
|
||||
'@eslint/eslintrc@2.1.4':
|
||||
|
|
@ -2921,7 +2932,7 @@ snapshots:
|
|||
'@codemirror/lint': 6.8.5
|
||||
'@codemirror/search': 6.5.10
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.36.5
|
||||
'@codemirror/view': 6.36.7
|
||||
|
||||
collect-v8-coverage@1.0.2: {}
|
||||
|
||||
|
|
@ -3155,7 +3166,7 @@ snapshots:
|
|||
|
||||
eslint@8.57.1:
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.6.1(eslint@8.57.1)
|
||||
'@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1)
|
||||
'@eslint-community/regexpp': 4.12.1
|
||||
'@eslint/eslintrc': 2.1.4
|
||||
'@eslint/js': 8.57.1
|
||||
|
|
@ -3993,25 +4004,25 @@ snapshots:
|
|||
|
||||
nwsapi@2.2.20: {}
|
||||
|
||||
obsidian-daily-notes-interface@0.9.4(@codemirror/state@6.5.2)(@codemirror/view@6.36.5):
|
||||
obsidian-daily-notes-interface@0.9.4(@codemirror/state@6.5.2)(@codemirror/view@6.36.7):
|
||||
dependencies:
|
||||
obsidian: https://codeload.github.com/obsidianmd/obsidian-api/tar.gz/9ab497e64afeac291a2546d2faeded6a7bed626a(@codemirror/state@6.5.2)(@codemirror/view@6.36.5)
|
||||
obsidian: https://codeload.github.com/obsidianmd/obsidian-api/tar.gz/9ab497e64afeac291a2546d2faeded6a7bed626a(@codemirror/state@6.5.2)(@codemirror/view@6.36.7)
|
||||
tslib: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- '@codemirror/state'
|
||||
- '@codemirror/view'
|
||||
|
||||
obsidian@1.8.7(@codemirror/state@6.5.2)(@codemirror/view@6.36.5):
|
||||
obsidian@1.8.7(@codemirror/state@6.5.2)(@codemirror/view@6.36.7):
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.36.5
|
||||
'@codemirror/view': 6.36.7
|
||||
'@types/codemirror': 5.60.8
|
||||
moment: 2.29.4
|
||||
|
||||
obsidian@https://codeload.github.com/obsidianmd/obsidian-api/tar.gz/9ab497e64afeac291a2546d2faeded6a7bed626a(@codemirror/state@6.5.2)(@codemirror/view@6.36.5):
|
||||
obsidian@https://codeload.github.com/obsidianmd/obsidian-api/tar.gz/9ab497e64afeac291a2546d2faeded6a7bed626a(@codemirror/state@6.5.2)(@codemirror/view@6.36.7):
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.36.5
|
||||
'@codemirror/view': 6.36.7
|
||||
'@types/codemirror': 5.60.8
|
||||
moment: 2.29.4
|
||||
|
||||
|
|
|
|||
|
|
@ -1,180 +1,251 @@
|
|||
// Mock for @codemirror/state
|
||||
|
||||
export class Text {
|
||||
content: string;
|
||||
|
||||
constructor(content: string = '') {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.content.length;
|
||||
}
|
||||
|
||||
sliceString(from: number, to: number) {
|
||||
return this.content.slice(from, to);
|
||||
}
|
||||
|
||||
line(lineNum: number) {
|
||||
const lines = this.content.split('\n');
|
||||
if (lineNum < 1 || lineNum > lines.length) {
|
||||
throw new Error(`Line ${lineNum} out of range`);
|
||||
}
|
||||
|
||||
const line = lines[lineNum - 1];
|
||||
let from = 0;
|
||||
for (let i = 0; i < lineNum - 1; i++) {
|
||||
from += lines[i].length + 1; // +1 for newline
|
||||
}
|
||||
|
||||
return {
|
||||
text: line,
|
||||
from,
|
||||
to: from + line.length,
|
||||
number: lineNum
|
||||
};
|
||||
}
|
||||
|
||||
get lines() {
|
||||
return this.content.split('\n').length;
|
||||
}
|
||||
|
||||
lineAt(pos: number) {
|
||||
let lineStart = 0;
|
||||
let lineEnd = 0;
|
||||
let lineNumber = 1;
|
||||
|
||||
const lines = this.content.split('\n');
|
||||
for (const line of lines) {
|
||||
lineEnd = lineStart + line.length;
|
||||
|
||||
if (pos >= lineStart && pos <= lineEnd) {
|
||||
return {
|
||||
text: line,
|
||||
from: lineStart,
|
||||
to: lineEnd,
|
||||
number: lineNumber
|
||||
};
|
||||
}
|
||||
|
||||
lineStart = lineEnd + 1; // +1 for newline
|
||||
lineNumber++;
|
||||
}
|
||||
|
||||
// Default to last line if position is beyond content
|
||||
return {
|
||||
text: lines[lines.length - 1] || '',
|
||||
from: lineStart - (lines[lines.length - 1]?.length || 0) - 1,
|
||||
to: lineStart,
|
||||
number: lines.length
|
||||
};
|
||||
}
|
||||
content: string;
|
||||
|
||||
constructor(content: string = "") {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.content.length;
|
||||
}
|
||||
|
||||
sliceString(from: number, to: number) {
|
||||
return this.content.slice(from, to);
|
||||
}
|
||||
|
||||
line(lineNum: number) {
|
||||
const lines = this.content.split("\n");
|
||||
if (lineNum < 1 || lineNum > lines.length) {
|
||||
throw new Error(`Line ${lineNum} out of range`);
|
||||
}
|
||||
|
||||
const line = lines[lineNum - 1];
|
||||
let from = 0;
|
||||
for (let i = 0; i < lineNum - 1; i++) {
|
||||
from += lines[i].length + 1; // +1 for newline
|
||||
}
|
||||
|
||||
return {
|
||||
text: line,
|
||||
from,
|
||||
to: from + line.length,
|
||||
number: lineNum,
|
||||
};
|
||||
}
|
||||
|
||||
get lines() {
|
||||
return this.content.split("\n").length;
|
||||
}
|
||||
|
||||
lineAt(pos: number) {
|
||||
let lineStart = 0;
|
||||
let lineEnd = 0;
|
||||
let lineNumber = 1;
|
||||
|
||||
const lines = this.content.split("\n");
|
||||
for (const line of lines) {
|
||||
lineEnd = lineStart + line.length;
|
||||
|
||||
if (pos >= lineStart && pos <= lineEnd) {
|
||||
return {
|
||||
text: line,
|
||||
from: lineStart,
|
||||
to: lineEnd,
|
||||
number: lineNumber,
|
||||
};
|
||||
}
|
||||
|
||||
lineStart = lineEnd + 1; // +1 for newline
|
||||
lineNumber++;
|
||||
}
|
||||
|
||||
// Default to last line if position is beyond content
|
||||
return {
|
||||
text: lines[lines.length - 1] || "",
|
||||
from: lineStart - (lines[lines.length - 1]?.length || 0) - 1,
|
||||
to: lineStart,
|
||||
number: lines.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class Changes {
|
||||
_changes: Array<{
|
||||
fromA: number;
|
||||
toA: number;
|
||||
fromB: number;
|
||||
toB: number;
|
||||
inserted: Text;
|
||||
}>;
|
||||
|
||||
constructor() {
|
||||
this._changes = [];
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this._changes.length;
|
||||
}
|
||||
|
||||
iterChanges(f: (fromA: number, toA: number, fromB: number, toB: number, inserted: Text) => void) {
|
||||
for (const change of this._changes) {
|
||||
f(change.fromA, change.toA, change.fromB, change.toB, change.inserted);
|
||||
}
|
||||
}
|
||||
_changes: Array<{
|
||||
fromA: number;
|
||||
toA: number;
|
||||
fromB: number;
|
||||
toB: number;
|
||||
inserted: Text;
|
||||
}>;
|
||||
|
||||
constructor() {
|
||||
this._changes = [];
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this._changes.length;
|
||||
}
|
||||
|
||||
iterChanges(
|
||||
f: (
|
||||
fromA: number,
|
||||
toA: number,
|
||||
fromB: number,
|
||||
toB: number,
|
||||
inserted: Text
|
||||
) => void
|
||||
) {
|
||||
for (const change of this._changes) {
|
||||
f(
|
||||
change.fromA,
|
||||
change.toA,
|
||||
change.fromB,
|
||||
change.toB,
|
||||
change.inserted
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Transaction {
|
||||
startState: EditorState;
|
||||
newDoc: Text;
|
||||
changes: Changes;
|
||||
selection: any;
|
||||
annotations: Map<Annotation<any>, any>;
|
||||
|
||||
constructor(options: {
|
||||
startState?: EditorState,
|
||||
newDoc?: Text,
|
||||
changes?: Changes,
|
||||
selection?: any,
|
||||
annotations?: Map<Annotation<any>, any>
|
||||
} = {}) {
|
||||
this.startState = options.startState || new EditorState();
|
||||
this.newDoc = options.newDoc || new Text();
|
||||
this.changes = options.changes || new Changes();
|
||||
this.selection = options.selection || null;
|
||||
this.annotations = options.annotations || new Map();
|
||||
}
|
||||
|
||||
annotation(annotation: Annotation<any>) {
|
||||
return this.annotations.get(annotation);
|
||||
}
|
||||
|
||||
get docChanged() {
|
||||
return this.changes.length > 0;
|
||||
}
|
||||
|
||||
isUserEvent(type: string) {
|
||||
return false;
|
||||
}
|
||||
startState: EditorState;
|
||||
newDoc: Text;
|
||||
changes: Changes;
|
||||
selection: any;
|
||||
annotations: Map<Annotation<any>, any>;
|
||||
|
||||
constructor(
|
||||
options: {
|
||||
startState?: EditorState;
|
||||
newDoc?: Text;
|
||||
changes?: Changes;
|
||||
selection?: any;
|
||||
annotations?: Map<Annotation<any>, any>;
|
||||
} = {}
|
||||
) {
|
||||
this.startState = options.startState || new EditorState();
|
||||
this.newDoc = options.newDoc || new Text();
|
||||
this.changes = options.changes || new Changes();
|
||||
this.selection = options.selection || null;
|
||||
this.annotations = options.annotations || new Map();
|
||||
}
|
||||
|
||||
annotation(annotation: Annotation<any>) {
|
||||
return this.annotations.get(annotation);
|
||||
}
|
||||
|
||||
get docChanged() {
|
||||
return this.changes.length > 0;
|
||||
}
|
||||
|
||||
isUserEvent(type: string) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class EditorState {
|
||||
doc: Text;
|
||||
selection: any;
|
||||
|
||||
constructor(config: { doc?: Text, selection?: any } = {}) {
|
||||
this.doc = config.doc || new Text();
|
||||
this.selection = config.selection || null;
|
||||
}
|
||||
|
||||
static create(config: any = {}) {
|
||||
return new EditorState(config);
|
||||
}
|
||||
|
||||
static transactionFilter = {
|
||||
of: (f: (tr: Transaction) => TransactionSpec) => {
|
||||
return {
|
||||
filter: f
|
||||
};
|
||||
}
|
||||
};
|
||||
doc: Text;
|
||||
selection: any;
|
||||
_fields: Map<any, any>;
|
||||
|
||||
constructor(
|
||||
config: { doc?: Text; selection?: any; extensions?: any[] } = {}
|
||||
) {
|
||||
this.doc = config.doc || new Text();
|
||||
this.selection = config.selection || null;
|
||||
this._fields = new Map();
|
||||
|
||||
if (config.extensions) {
|
||||
config.extensions.forEach((ext) => {
|
||||
if (ext && ext.hasOwnProperty("provides")) {
|
||||
const fieldProvider = ext.provides;
|
||||
if (
|
||||
fieldProvider &&
|
||||
fieldProvider.field &&
|
||||
fieldProvider.create
|
||||
) {
|
||||
this._fields.set(
|
||||
fieldProvider.field,
|
||||
fieldProvider.create(this)
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
ext &&
|
||||
ext.hasOwnProperty("field") &&
|
||||
ext.hasOwnProperty("create")
|
||||
) {
|
||||
this._fields.set(ext.field, ext.create(this));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
field<T>(field: any /* StateField<T> | Facet<any, T> */): T | undefined {
|
||||
return this._fields.get(field);
|
||||
}
|
||||
|
||||
static create(config: any = {}) {
|
||||
return new EditorState(config);
|
||||
}
|
||||
|
||||
static transactionFilter = {
|
||||
of: (f: (tr: Transaction) => TransactionSpec) => {
|
||||
return {
|
||||
filter: f,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export class Annotation<T> {
|
||||
constructor(public name: string) {}
|
||||
|
||||
of(value: T) {
|
||||
return value;
|
||||
}
|
||||
|
||||
static define<T>() {
|
||||
return new Annotation<T>('mock-annotation');
|
||||
}
|
||||
constructor(public name: string) {}
|
||||
|
||||
of(value: T) {
|
||||
return value;
|
||||
}
|
||||
|
||||
static define<T>() {
|
||||
return new Annotation<T>("mock-annotation");
|
||||
}
|
||||
}
|
||||
|
||||
export interface TransactionSpec {
|
||||
changes?: any;
|
||||
selection?: any;
|
||||
annotations?: any;
|
||||
changes?: any;
|
||||
selection?: any;
|
||||
annotations?: any;
|
||||
}
|
||||
|
||||
export const StateEffect = {
|
||||
define: () => ({
|
||||
of: (value: any) => ({ value })
|
||||
})
|
||||
};
|
||||
define: () => ({
|
||||
of: (value: any) => ({ value }),
|
||||
}),
|
||||
};
|
||||
|
||||
// Add a mock for EditorSelection
|
||||
export const EditorSelection = {
|
||||
single: jest.fn((anchor: number, head?: number) => {
|
||||
// Return a mock SelectionRange or similar structure
|
||||
// The specific structure depends on what properties your tests need
|
||||
const resolvedHead = head ?? anchor;
|
||||
return {
|
||||
anchor: anchor,
|
||||
head: resolvedHead,
|
||||
from: Math.min(anchor, resolvedHead),
|
||||
to: Math.max(anchor, resolvedHead),
|
||||
empty: anchor === resolvedHead,
|
||||
// You might need to add other properties based on actual usage:
|
||||
// main: { anchor, head: resolvedHead }, // Mock main selection range
|
||||
// ranges: [{ anchor, head: resolvedHead }], // Mock ranges array
|
||||
// ... other methods or properties EditorSelection/SelectionRange might need
|
||||
};
|
||||
}),
|
||||
// If your code also uses other static methods or properties of EditorSelection,
|
||||
// such as EditorSelection.range(), add corresponding mocks here as well
|
||||
// range: jest.fn((anchor: number, head: number) => { ... }),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -35,6 +35,16 @@ export class App {
|
|||
openFile: function () {},
|
||||
};
|
||||
},
|
||||
|
||||
getActiveFile: function () {
|
||||
return {
|
||||
path: "mockFile.md",
|
||||
// Add other TFile properties if necessary for the tests
|
||||
name: "mockFile.md",
|
||||
basename: "mockFile",
|
||||
extension: "md",
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
fileManager = {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,3 @@
|
|||
import { App } from "obsidian";
|
||||
import {
|
||||
Text,
|
||||
Transaction,
|
||||
TransactionSpec,
|
||||
EditorState,
|
||||
ChangeSet,
|
||||
Annotation,
|
||||
} from "@codemirror/state";
|
||||
import TaskProgressBarPlugin from ".."; // Adjust the import path as necessary
|
||||
import {
|
||||
handleParentTaskUpdateTransaction,
|
||||
findTaskStatusChange,
|
||||
|
|
@ -15,239 +5,19 @@ import {
|
|||
areAllSiblingsCompleted,
|
||||
anySiblingWithStatus,
|
||||
getParentTaskStatus,
|
||||
taskStatusChangeAnnotation, // Import the actual annotation
|
||||
} from "../editor-ext/autoCompleteParent"; // Adjust the import path as necessary
|
||||
import { TaskProgressBarSettings } from "src/common/setting-definition";
|
||||
import { buildIndentString, getTabSize } from "../utils";
|
||||
import { buildIndentString } from "../utils";
|
||||
import {
|
||||
createMockTransaction,
|
||||
createMockApp,
|
||||
createMockPlugin,
|
||||
createMockText,
|
||||
mockParentTaskStatusChangeAnnotation,
|
||||
} from "./mockUtils";
|
||||
|
||||
// --- Mock Setup ---
|
||||
|
||||
// Mock Annotation Type
|
||||
const mockAnnotationType = {
|
||||
of: jest.fn().mockImplementation((value: string) => ({
|
||||
type: mockAnnotationType,
|
||||
value,
|
||||
})),
|
||||
};
|
||||
// Use the actual annotation object from the source file for checks
|
||||
const mockParentTaskStatusChangeAnnotation = taskStatusChangeAnnotation;
|
||||
|
||||
// Mock Text Object
|
||||
const createMockText = (content: string): Text => {
|
||||
const lines = content.split("\n");
|
||||
const doc = {
|
||||
toString: () => content,
|
||||
length: content.length,
|
||||
lines: lines.length,
|
||||
line: jest.fn((lineNum: number) => {
|
||||
if (lineNum < 1 || lineNum > lines.length) {
|
||||
throw new Error(
|
||||
`Line ${lineNum} out of range (1-${lines.length})`
|
||||
);
|
||||
}
|
||||
const text = lines[lineNum - 1];
|
||||
let from = 0;
|
||||
for (let i = 0; i < lineNum - 1; i++) {
|
||||
from += lines[i].length + 1; // +1 for newline
|
||||
}
|
||||
return {
|
||||
text: text,
|
||||
from,
|
||||
to: from + text.length,
|
||||
number: lineNum,
|
||||
length: text.length,
|
||||
};
|
||||
}),
|
||||
lineAt: jest.fn((pos: number) => {
|
||||
if (pos < 0 || pos > content.length) {
|
||||
throw new Error(
|
||||
`Position ${pos} out of range (0-${content.length})`
|
||||
);
|
||||
}
|
||||
let currentPos = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const lineLength = lines[i].length;
|
||||
const lineStart = currentPos;
|
||||
const lineEnd = currentPos + lineLength;
|
||||
if (pos >= lineStart && pos <= lineEnd) {
|
||||
return {
|
||||
text: lines[i],
|
||||
from: lineStart,
|
||||
to: lineEnd,
|
||||
number: i + 1,
|
||||
length: lineLength,
|
||||
};
|
||||
}
|
||||
currentPos += lineLength + 1; // +1 for newline
|
||||
}
|
||||
// Handle edge case for position at the very end
|
||||
if (pos === content.length && lines.length > 0) {
|
||||
const lastLineIndex = lines.length - 1;
|
||||
const lastLine = lines[lastLineIndex];
|
||||
let from = 0;
|
||||
for (let i = 0; i < lastLineIndex; i++) {
|
||||
from += lines[i].length + 1;
|
||||
}
|
||||
return {
|
||||
text: lastLine,
|
||||
from: from,
|
||||
to: from + lastLine.length,
|
||||
number: lines.length,
|
||||
length: lastLine.length,
|
||||
};
|
||||
}
|
||||
throw new Error(`Could not find line at pos ${pos}`);
|
||||
}),
|
||||
// Add sliceString if needed by iterChanges simulation
|
||||
sliceString: jest.fn((from: number, to: number) =>
|
||||
content.slice(from, to)
|
||||
),
|
||||
};
|
||||
// @ts-ignore - Add self-reference for Text methods if necessary
|
||||
doc.doc = doc;
|
||||
return doc as Text;
|
||||
};
|
||||
|
||||
// Mock ChangeSet
|
||||
const createMockChangeSet = (doc: Text, changes: any[] = []): ChangeSet => {
|
||||
// Basic mock, might need refinement based on actual usage
|
||||
return {
|
||||
length: changes.length, // Length of the new document
|
||||
// @ts-ignore
|
||||
iterChanges: jest.fn((callback) => {
|
||||
changes.forEach((change) => {
|
||||
callback(
|
||||
change.fromA,
|
||||
change.toA,
|
||||
change.fromB,
|
||||
change.toB,
|
||||
createMockText(change.insertedText || "") // inserted text needs to be a Text object
|
||||
);
|
||||
});
|
||||
}),
|
||||
// Add other ChangeSet methods if needed: mapDesc, compose, mapPos, etc.
|
||||
} as ChangeSet;
|
||||
};
|
||||
|
||||
// Mock Transaction Object
|
||||
const createMockTransaction = (options: {
|
||||
startStateDocContent?: string;
|
||||
newDocContent?: string;
|
||||
changes?: {
|
||||
fromA: number;
|
||||
toA: number;
|
||||
fromB: number;
|
||||
toB: number;
|
||||
insertedText?: string;
|
||||
}[];
|
||||
docChanged?: boolean;
|
||||
isUserEvent?: string | false; // e.g., 'input.paste' or false
|
||||
annotations?: any[];
|
||||
selection?: { anchor: number; head: number };
|
||||
}): Transaction => {
|
||||
const startDoc = createMockText(options.startStateDocContent ?? "");
|
||||
const newDoc = createMockText(
|
||||
options.newDocContent ?? options.startStateDocContent ?? ""
|
||||
);
|
||||
const changeSet = createMockChangeSet(newDoc, options.changes);
|
||||
|
||||
const mockTr = {
|
||||
startState: { doc: startDoc /* other state props */ } as EditorState,
|
||||
newDoc: newDoc,
|
||||
changes: changeSet,
|
||||
docChanged:
|
||||
options.docChanged !== undefined
|
||||
? options.docChanged
|
||||
: !!options.changes?.length,
|
||||
isUserEvent: jest.fn((type: string) => {
|
||||
if (options.isUserEvent === false) return false;
|
||||
return options.isUserEvent === type;
|
||||
}),
|
||||
annotation: jest.fn(<T>(type: Annotation<T>): T | undefined => {
|
||||
const found = options.annotations?.find((ann) => ann.type === type);
|
||||
return found ? found.value : undefined;
|
||||
}),
|
||||
selection: options.selection || { anchor: 0, head: 0 },
|
||||
// Add other Transaction properties/methods if needed (effects, state, etc.)
|
||||
};
|
||||
|
||||
// @ts-ignore - Allow adding mock state for testing purposes
|
||||
mockTr.state = { doc: newDoc };
|
||||
|
||||
return mockTr as unknown as Transaction;
|
||||
};
|
||||
|
||||
// Mock App Object
|
||||
const createMockApp = (tabSize: number = 4): App => {
|
||||
const app = new App();
|
||||
// Override vault.getConfig if a specific tab size is provided
|
||||
if (tabSize !== 4) {
|
||||
// @ts-ignore - Override the getConfig function to return the specified tab size
|
||||
app.vault.getConfig = jest.fn((key: string) => {
|
||||
if (key === "tabSize") {
|
||||
return tabSize;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
return app;
|
||||
};
|
||||
|
||||
// Mock Plugin Object
|
||||
const createMockPlugin = (
|
||||
settings: Partial<{
|
||||
markParentInProgressWhenPartiallyComplete: boolean;
|
||||
taskStatuses: { inProgress: string; completed: string };
|
||||
workflow: {
|
||||
enableWorkflow: boolean;
|
||||
autoRemoveLastStageMarker: boolean;
|
||||
taskCountAffectsParent: boolean;
|
||||
autoAddTimestamp: boolean;
|
||||
timestampFormat: string;
|
||||
removeTimestampOnTransition: boolean;
|
||||
calculateSpentTime: boolean;
|
||||
spentTimeFormat: string;
|
||||
definitions: any[];
|
||||
autoAddNextTask: boolean;
|
||||
calculateFullSpentTime: boolean;
|
||||
};
|
||||
}> = {}
|
||||
): TaskProgressBarPlugin => {
|
||||
const defaults = {
|
||||
markParentInProgressWhenPartiallyComplete: true,
|
||||
taskStatuses: {
|
||||
inProgress: "/",
|
||||
completed: "x|X",
|
||||
},
|
||||
workflow: {
|
||||
enableWorkflow: false,
|
||||
autoRemoveLastStageMarker: true,
|
||||
taskCountAffectsParent: true,
|
||||
autoAddTimestamp: false,
|
||||
timestampFormat: "YYYY-MM-DD HH:mm:ss",
|
||||
removeTimestampOnTransition: false,
|
||||
calculateSpentTime: false,
|
||||
spentTimeFormat: "HH:mm",
|
||||
definitions: [],
|
||||
autoAddNextTask: false,
|
||||
calculateFullSpentTime: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Deep merge settings
|
||||
const mergedSettings = {
|
||||
...defaults,
|
||||
...settings,
|
||||
taskStatuses: { ...defaults.taskStatuses, ...settings.taskStatuses },
|
||||
workflow: { ...defaults.workflow, ...settings.workflow },
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
return {
|
||||
settings: mergedSettings as unknown as TaskProgressBarSettings,
|
||||
app: new App(), // Add app property with mock App instance
|
||||
};
|
||||
};
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
|
|
@ -639,7 +409,13 @@ describe("handleParentTaskUpdateTransaction (Integration)", () => {
|
|||
it("should mark parent as in progress when a child is unchecked (if setting enabled)", () => {
|
||||
const mockPlugin = createMockPlugin({
|
||||
markParentInProgressWhenPartiallyComplete: true,
|
||||
taskStatuses: { inProgress: "/", completed: "x" },
|
||||
taskStatuses: {
|
||||
inProgress: "/",
|
||||
completed: "x",
|
||||
abandoned: "-",
|
||||
planned: "?",
|
||||
notStarted: " ",
|
||||
},
|
||||
});
|
||||
const tr = createMockTransaction({
|
||||
startStateDocContent: "- [x] Parent\n - [x] Child",
|
||||
|
|
@ -690,7 +466,13 @@ describe("handleParentTaskUpdateTransaction (Integration)", () => {
|
|||
it("should mark parent as in progress when first child gets a status (if setting enabled)", () => {
|
||||
const mockPlugin = createMockPlugin({
|
||||
markParentInProgressWhenPartiallyComplete: true,
|
||||
taskStatuses: { inProgress: "/", completed: "x" },
|
||||
taskStatuses: {
|
||||
inProgress: "/",
|
||||
completed: "x",
|
||||
abandoned: "-",
|
||||
planned: "?",
|
||||
notStarted: " ",
|
||||
},
|
||||
});
|
||||
const indent = buildIndentString(createMockApp());
|
||||
const tr = createMockTransaction({
|
||||
|
|
@ -742,7 +524,13 @@ describe("handleParentTaskUpdateTransaction (Integration)", () => {
|
|||
it("should NOT mark parent as in progress if parent already has a status", () => {
|
||||
const mockPlugin = createMockPlugin({
|
||||
markParentInProgressWhenPartiallyComplete: true,
|
||||
taskStatuses: { inProgress: "/", completed: "x" },
|
||||
taskStatuses: {
|
||||
inProgress: "/",
|
||||
completed: "x",
|
||||
abandoned: "-",
|
||||
planned: "?",
|
||||
notStarted: " ",
|
||||
},
|
||||
});
|
||||
const tr = createMockTransaction({
|
||||
startStateDocContent:
|
||||
|
|
@ -797,7 +585,13 @@ describe("handleParentTaskUpdateTransaction (Integration)", () => {
|
|||
const indent = buildIndentString(createMockApp());
|
||||
const mockPlugin = createMockPlugin({
|
||||
markParentInProgressWhenPartiallyComplete: true,
|
||||
taskStatuses: { inProgress: "/", completed: "x" },
|
||||
taskStatuses: {
|
||||
inProgress: "/",
|
||||
completed: "x",
|
||||
abandoned: "-",
|
||||
planned: "?",
|
||||
notStarted: " ",
|
||||
},
|
||||
});
|
||||
const tr = createMockTransaction({
|
||||
startStateDocContent: "- [ ] Parent\n" + `${indent}- [ ] Child`,
|
||||
|
|
@ -831,7 +625,13 @@ describe("handleParentTaskUpdateTransaction (Integration)", () => {
|
|||
it("should mark parent as in progress when one child is completed but others remain incomplete", () => {
|
||||
const mockPlugin = createMockPlugin({
|
||||
markParentInProgressWhenPartiallyComplete: true,
|
||||
taskStatuses: { inProgress: "/", completed: "x" },
|
||||
taskStatuses: {
|
||||
inProgress: "/",
|
||||
completed: "x",
|
||||
abandoned: "-",
|
||||
planned: "?",
|
||||
notStarted: " ",
|
||||
},
|
||||
});
|
||||
const indent = buildIndentString(createMockApp());
|
||||
const tr = createMockTransaction({
|
||||
|
|
@ -981,14 +781,9 @@ describe("handleParentTaskUpdateTransaction (Integration)", () => {
|
|||
mockPlugin
|
||||
);
|
||||
|
||||
console.log((result as any).changes);
|
||||
|
||||
// The function should identify and prevent such accidental parent status changes
|
||||
expect(result).toBe(tr);
|
||||
expect(result.changes).toEqual(tr.changes);
|
||||
// Verify no new changes were added to modify parent task status
|
||||
// Use type assertion to access changes property in test mocks
|
||||
expect((result as any).changes.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,14 @@
|
|||
import { App, EditorSelection } from "obsidian";
|
||||
import {
|
||||
Text,
|
||||
Transaction,
|
||||
TransactionSpec,
|
||||
EditorState,
|
||||
ChangeSet,
|
||||
Annotation,
|
||||
ChangeSpec,
|
||||
AnnotationType,
|
||||
} from "@codemirror/state";
|
||||
import TaskProgressBarPlugin from ".."; // Adjust the import path as necessary
|
||||
createMockTransaction,
|
||||
createMockApp,
|
||||
createMockPlugin,
|
||||
} from "./mockUtils";
|
||||
import {
|
||||
handleCycleCompleteStatusTransaction,
|
||||
findTaskStatusChanges,
|
||||
taskStatusChangeAnnotation, // Import the actual annotation
|
||||
priorityChangeAnnotation, // Import priority annotation
|
||||
} from "../editor-ext/cycleCompleteStatus"; // Adjust the import path as necessary
|
||||
import {
|
||||
TaskProgressBarSettings,
|
||||
WorkflowSettings,
|
||||
} from "../common/setting-definition";
|
||||
import { buildIndentString } from "../utils";
|
||||
|
||||
// --- Mock Setup (Reusing mocks from autoCompleteParent.test.ts) ---
|
||||
|
|
@ -31,319 +20,6 @@ const mockAnnotationType = {
|
|||
value,
|
||||
})),
|
||||
};
|
||||
// Use the actual annotation objects from the source file for checks
|
||||
const mockTaskStatusAnnotation = taskStatusChangeAnnotation;
|
||||
const mockPriorityAnnotation = priorityChangeAnnotation;
|
||||
|
||||
// Mock Text Object
|
||||
const createMockText = (content: string): Text => {
|
||||
const lines = content.split("\n");
|
||||
const doc = {
|
||||
toString: () => content,
|
||||
length: content.length,
|
||||
lines: lines.length,
|
||||
line: jest.fn((lineNum: number) => {
|
||||
if (lineNum < 1 || lineNum > lines.length) {
|
||||
throw new Error(
|
||||
`Line ${lineNum} out of range (1-${lines.length})`
|
||||
);
|
||||
}
|
||||
const text = lines[lineNum - 1];
|
||||
let from = 0;
|
||||
for (let i = 0; i < lineNum - 1; i++) {
|
||||
from += lines[i].length + 1; // +1 for newline
|
||||
}
|
||||
return {
|
||||
text: text,
|
||||
from,
|
||||
to: from + text.length,
|
||||
number: lineNum,
|
||||
length: text.length,
|
||||
};
|
||||
}),
|
||||
lineAt: jest.fn((pos: number) => {
|
||||
// Ensure pos is within valid range
|
||||
pos = Math.max(0, Math.min(pos, content.length));
|
||||
let currentPos = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const lineLength = lines[i].length;
|
||||
const lineStart = currentPos;
|
||||
const lineEnd = currentPos + lineLength;
|
||||
// Check if pos is within the current line or at the very end of the document
|
||||
if (pos >= lineStart && pos <= lineEnd) {
|
||||
return {
|
||||
text: lines[i],
|
||||
from: lineStart,
|
||||
to: lineEnd,
|
||||
number: i + 1,
|
||||
length: lineLength,
|
||||
};
|
||||
}
|
||||
currentPos += lineLength + 1; // +1 for newline
|
||||
}
|
||||
// Handle edge case: position at the very end of the file after the last newline
|
||||
if (
|
||||
pos === content.length &&
|
||||
lines.length > 0 &&
|
||||
content.endsWith("\n")
|
||||
) {
|
||||
const lastLineIndex = lines.length - 1;
|
||||
const lastLine = lines[lastLineIndex];
|
||||
let from = content.length - lastLine.length - 1; // Position after the last newline
|
||||
return {
|
||||
text: lastLine,
|
||||
from: from,
|
||||
to: from + lastLine.length,
|
||||
number: lines.length,
|
||||
length: lastLine.length,
|
||||
};
|
||||
} else if (
|
||||
pos === content.length &&
|
||||
lines.length > 0 &&
|
||||
!content.endsWith("\n")
|
||||
) {
|
||||
// Position exactly at the end of the last line (no trailing newline)
|
||||
const lastLineIndex = lines.length - 1;
|
||||
const lastLine = lines[lastLineIndex];
|
||||
let from = 0;
|
||||
for (let i = 0; i < lastLineIndex; i++) {
|
||||
from += lines[i].length + 1;
|
||||
}
|
||||
return {
|
||||
text: lastLine,
|
||||
from: from,
|
||||
to: from + lastLine.length,
|
||||
number: lines.length,
|
||||
length: lastLine.length,
|
||||
};
|
||||
}
|
||||
// If the content is empty or pos is 0 in an empty doc
|
||||
if (content === "" && pos === 0) {
|
||||
return {
|
||||
text: "",
|
||||
from: 0,
|
||||
to: 0,
|
||||
number: 1,
|
||||
length: 0,
|
||||
};
|
||||
}
|
||||
throw new Error(
|
||||
`Could not find line at pos ${pos} in content: "${content}"`
|
||||
);
|
||||
}),
|
||||
sliceString: jest.fn((from: number, to: number) =>
|
||||
content.slice(from, to)
|
||||
),
|
||||
};
|
||||
// @ts-ignore - Add self-reference for Text methods if necessary
|
||||
doc.doc = doc;
|
||||
return doc as Text;
|
||||
};
|
||||
|
||||
// Mock ChangeSet
|
||||
const createMockChangeSet = (doc: Text, changes: any[] = []): ChangeSet => {
|
||||
return {
|
||||
length: doc.length,
|
||||
// @ts-ignore
|
||||
iterChanges: jest.fn(
|
||||
(
|
||||
callback: (
|
||||
fromA: number,
|
||||
toA: number,
|
||||
fromB: number,
|
||||
toB: number,
|
||||
inserted: Text
|
||||
) => void
|
||||
) => {
|
||||
changes.forEach((change) => {
|
||||
// Basic validation to prevent errors on undefined values
|
||||
const fromA = change.fromA ?? 0;
|
||||
const toA = change.toA ?? fromA;
|
||||
const fromB = change.fromB ?? 0;
|
||||
const insertedText = change.insertedText ?? "";
|
||||
const toB = change.toB ?? fromB + insertedText.length;
|
||||
callback(
|
||||
fromA,
|
||||
toA,
|
||||
fromB,
|
||||
toB,
|
||||
createMockText(insertedText) // inserted text needs to be a Text object
|
||||
);
|
||||
});
|
||||
}
|
||||
),
|
||||
// Add other necessary ChangeSet methods if needed, even if mocked simply
|
||||
// @ts-ignore
|
||||
mapDesc: jest.fn(() => ({
|
||||
/* mock */
|
||||
})),
|
||||
// @ts-ignore
|
||||
compose: jest.fn(() => ({
|
||||
/* mock */
|
||||
})),
|
||||
// @ts-ignore
|
||||
mapPos: jest.fn(() => 0),
|
||||
// @ts-ignore
|
||||
toJSON: jest.fn(() => ({
|
||||
/* mock */
|
||||
})),
|
||||
// @ts-ignore
|
||||
any: jest.fn(() => false),
|
||||
// @ts-ignore
|
||||
get desc() {
|
||||
return {
|
||||
/* mock */
|
||||
};
|
||||
},
|
||||
// @ts-ignore
|
||||
get empty() {
|
||||
return changes.length === 0;
|
||||
},
|
||||
// ... and potentially others like 'apply', 'invert', etc. if used
|
||||
} as unknown as ChangeSet;
|
||||
};
|
||||
|
||||
// Mock Transaction Object
|
||||
const createMockTransaction = (options: {
|
||||
startStateDocContent?: string;
|
||||
newDocContent?: string;
|
||||
changes?: {
|
||||
fromA: number;
|
||||
toA: number;
|
||||
fromB: number;
|
||||
toB: number;
|
||||
insertedText?: string;
|
||||
}[];
|
||||
docChanged?: boolean;
|
||||
isUserEvent?: string | false; // e.g., 'input.paste' or false
|
||||
annotations?: { type: AnnotationType<any>; value: any }[];
|
||||
selection?: { anchor: number; head: number };
|
||||
}): Transaction => {
|
||||
const startDoc = createMockText(options.startStateDocContent ?? "");
|
||||
const newDoc = createMockText(
|
||||
options.newDocContent ?? options.startStateDocContent ?? ""
|
||||
);
|
||||
// Ensure changes array exists and is valid
|
||||
const validChanges =
|
||||
options.changes?.map((c) => ({
|
||||
fromA: c.fromA ?? 0,
|
||||
toA: c.toA ?? c.fromA ?? 0,
|
||||
fromB: c.fromB ?? 0,
|
||||
insertedText: c.insertedText ?? "",
|
||||
toB: c.toB ?? (c.fromB ?? 0) + (c.insertedText ?? "").length,
|
||||
})) || [];
|
||||
const changeSet = createMockChangeSet(newDoc, validChanges);
|
||||
|
||||
// Create a proper EditorSelection object instead of just using an anchor/head object
|
||||
const selectionObj = options.selection || { anchor: 0, head: 0 };
|
||||
const editorSelection = {
|
||||
ranges: [{ anchor: selectionObj.anchor, head: selectionObj.head }],
|
||||
mainIndex: 0,
|
||||
map: jest.fn(() => editorSelection),
|
||||
eq: jest.fn(() => true),
|
||||
main: { anchor: selectionObj.anchor, head: selectionObj.head },
|
||||
extend: jest.fn(() => editorSelection),
|
||||
update: jest.fn(() => editorSelection),
|
||||
toJSON: jest.fn(() => ({})),
|
||||
toString: jest.fn(() => ""),
|
||||
filter: jest.fn(() => editorSelection),
|
||||
} as unknown as EditorSelection;
|
||||
|
||||
const mockTr = {
|
||||
startState: { doc: startDoc } as EditorState,
|
||||
newDoc: newDoc,
|
||||
changes: changeSet,
|
||||
docChanged:
|
||||
options.docChanged !== undefined
|
||||
? options.docChanged
|
||||
: !!validChanges.length,
|
||||
isUserEvent: jest.fn((type: string) => {
|
||||
if (options.isUserEvent === false) return false;
|
||||
return options.isUserEvent === type;
|
||||
}),
|
||||
annotation: jest.fn(<T>(type: AnnotationType<T>): T | undefined => {
|
||||
const found = options.annotations?.find((ann) => ann.type === type);
|
||||
return found ? found.value : undefined;
|
||||
}),
|
||||
selection: editorSelection,
|
||||
// Add required Transaction properties with basic mocks
|
||||
effects: [],
|
||||
scrollIntoView: false,
|
||||
newSelection: editorSelection,
|
||||
state: {
|
||||
doc: newDoc,
|
||||
selection: editorSelection,
|
||||
// Add other required state properties with basic mocks
|
||||
facet: jest.fn(() => null),
|
||||
field: jest.fn(() => null),
|
||||
fieldInvalidated: jest.fn(() => false),
|
||||
toJSON: jest.fn(() => ({})),
|
||||
replaceSelection: jest.fn(),
|
||||
changeByRange: jest.fn(),
|
||||
changes: jest.fn(),
|
||||
toText: jest.fn(() => newDoc),
|
||||
// @ts-ignore
|
||||
values: [],
|
||||
// @ts-ignore
|
||||
apply: jest.fn(() => ({})),
|
||||
// @ts-ignore
|
||||
update: jest.fn(() => ({})),
|
||||
// @ts-ignore
|
||||
sliceDoc: jest.fn(() => ""),
|
||||
} as unknown as EditorState,
|
||||
reconfigured: false,
|
||||
};
|
||||
|
||||
return mockTr as unknown as Transaction;
|
||||
};
|
||||
|
||||
// Mock App Object (basic, only needs vault for getTasksAPI check potentially)
|
||||
const createMockApp = (): App => {
|
||||
const app = new App();
|
||||
return app;
|
||||
};
|
||||
|
||||
// Mock Plugin Object
|
||||
const createMockPlugin = (
|
||||
settings: Partial<{
|
||||
taskStatusCycle: string[];
|
||||
taskStatusMarks: Record<string, string>;
|
||||
excludeMarksFromCycle: string[];
|
||||
}> = {}
|
||||
): TaskProgressBarPlugin => {
|
||||
const defaults: Partial<TaskProgressBarSettings> = {
|
||||
taskStatusCycle: ["TODO", "IN_PROGRESS", "DONE"],
|
||||
taskStatusMarks: { TODO: " ", IN_PROGRESS: "/", DONE: "x" },
|
||||
excludeMarksFromCycle: [],
|
||||
// Add other relevant defaults needed for the tests
|
||||
workflow: {
|
||||
enableWorkflow: false,
|
||||
autoRemoveLastStageMarker: true,
|
||||
autoAddTimestamp: false,
|
||||
timestampFormat: "YYYY-MM-DD HH:mm:ss",
|
||||
removeTimestampOnTransition: false,
|
||||
calculateSpentTime: false,
|
||||
spentTimeFormat: "HH:mm",
|
||||
definitions: [],
|
||||
autoAddNextTask: false,
|
||||
calculateFullSpentTime: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Merge settings
|
||||
const mergedSettings = {
|
||||
...defaults,
|
||||
...settings,
|
||||
};
|
||||
|
||||
// Return the plugin with app property
|
||||
return {
|
||||
settings: mergedSettings as TaskProgressBarSettings,
|
||||
app: new App(),
|
||||
} as TaskProgressBarPlugin;
|
||||
};
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
describe("cycleCompleteStatus Helpers", () => {
|
||||
describe("findTaskStatusChanges", () => {
|
||||
|
|
@ -1227,7 +903,6 @@ describe("handleCycleCompleteStatusTransaction (Integration)", () => {
|
|||
mockApp,
|
||||
mockPlugin
|
||||
);
|
||||
console.log(result.changes);
|
||||
expect(result.annotations).not.toBe("taskStatusChange");
|
||||
expect(result).toBe(tr);
|
||||
});
|
||||
|
|
|
|||
148
src/__tests__/findContinuousTaskBlocks.test.ts
Normal file
148
src/__tests__/findContinuousTaskBlocks.test.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import {
|
||||
findContinuousTaskBlocks,
|
||||
SortableTask,
|
||||
SortableTaskStatus,
|
||||
} from "../commands/sortTaskCommands";
|
||||
|
||||
// 创建SortableTask的辅助函数
|
||||
function createMockTask(
|
||||
lineNumber: number,
|
||||
indentation: number = 0,
|
||||
children: SortableTask[] = [],
|
||||
completed: boolean = false
|
||||
): SortableTask {
|
||||
const status = completed ? "x" : " ";
|
||||
return {
|
||||
id: `test-${lineNumber}`,
|
||||
lineNumber,
|
||||
indentation,
|
||||
children,
|
||||
parent: undefined,
|
||||
calculatedStatus: completed
|
||||
? SortableTaskStatus.Completed
|
||||
: SortableTaskStatus.Incomplete,
|
||||
originalMarkdown: `${" ".repeat(
|
||||
indentation
|
||||
)}- [${status}] Task at line ${lineNumber}`,
|
||||
status,
|
||||
completed,
|
||||
content: `Task at line ${lineNumber}`,
|
||||
tags: [],
|
||||
} as SortableTask;
|
||||
}
|
||||
|
||||
describe("findContinuousTaskBlocks", () => {
|
||||
it("应该能够识别连续的任务块", () => {
|
||||
// 创建模拟任务数据
|
||||
const mockTasks: SortableTask[] = [
|
||||
// 第一个任务块(连续行号:0,1,2)
|
||||
createMockTask(0),
|
||||
createMockTask(1),
|
||||
createMockTask(2),
|
||||
|
||||
// 第二个任务块(连续行号:5,6)- 与第一个块之间有空行
|
||||
createMockTask(5),
|
||||
createMockTask(6),
|
||||
];
|
||||
|
||||
// 执行函数
|
||||
const blocks = findContinuousTaskBlocks(mockTasks);
|
||||
|
||||
// 验证结果
|
||||
expect(blocks.length).toBe(2); // 应该识别出两个不连续的任务块
|
||||
expect(blocks[0].length).toBe(3); // 第一个块有3个任务
|
||||
expect(blocks[1].length).toBe(2); // 第二个块有2个任务
|
||||
|
||||
// 验证块中任务的行号
|
||||
expect(blocks[0].map((t) => t.lineNumber)).toEqual([0, 1, 2]);
|
||||
expect(blocks[1].map((t) => t.lineNumber)).toEqual([5, 6]);
|
||||
});
|
||||
|
||||
it("应该正确处理带有子任务的任务块", () => {
|
||||
// 创建子任务
|
||||
const childTask1 = createMockTask(1, 2);
|
||||
const childTask2 = createMockTask(2, 2);
|
||||
|
||||
// 创建带有子任务的模拟数据
|
||||
const parentTask = createMockTask(0, 0, [childTask1, childTask2]);
|
||||
childTask1.parent = parentTask;
|
||||
childTask2.parent = parentTask;
|
||||
|
||||
const mockTasks: SortableTask[] = [
|
||||
// 任务块1:父任务(包含子任务)
|
||||
parentTask,
|
||||
|
||||
// 任务块2:独立任务
|
||||
createMockTask(5),
|
||||
];
|
||||
|
||||
// 执行函数
|
||||
const blocks = findContinuousTaskBlocks(mockTasks);
|
||||
|
||||
// 验证结果
|
||||
expect(blocks.length).toBe(2); // 应该识别出两个不连续的任务块
|
||||
|
||||
// 验证第一个块包含父任务
|
||||
expect(blocks[0].length).toBe(1);
|
||||
expect(blocks[0][0].lineNumber).toBe(0);
|
||||
expect(blocks[0][0].children.length).toBe(2);
|
||||
expect(blocks[0][0].children[0].lineNumber).toBe(1);
|
||||
expect(blocks[0][0].children[1].lineNumber).toBe(2);
|
||||
|
||||
// 验证第二个块包含独立任务
|
||||
expect(blocks[1].length).toBe(1);
|
||||
expect(blocks[1][0].lineNumber).toBe(5);
|
||||
});
|
||||
|
||||
it("应该将任务及其子任务视为一个连续块", () => {
|
||||
// 创建一个带有子任务的任务,子任务在不连续的行
|
||||
const child1 = createMockTask(2, 2);
|
||||
const child2 = createMockTask(4, 2);
|
||||
|
||||
const parent1 = createMockTask(0, 0, [child1]);
|
||||
child1.parent = parent1;
|
||||
|
||||
const parent2 = createMockTask(3, 0, [child2]);
|
||||
child2.parent = parent2;
|
||||
|
||||
const mockTasks: SortableTask[] = [parent1, parent2];
|
||||
|
||||
// 执行函数 - 行号是 0, 2, 3, 4
|
||||
const blocks = findContinuousTaskBlocks(mockTasks);
|
||||
|
||||
// 验证结果 - 应该是一个连续块,因为父任务的最大行号 + 1 >= 下一个任务的行号
|
||||
expect(blocks.length).toBe(1); // 应该识别为一个连续块
|
||||
expect(blocks[0].length).toBe(2); // 包含两个父任务
|
||||
});
|
||||
|
||||
it("在没有任务的情况下应返回空数组", () => {
|
||||
const emptyTasks: SortableTask[] = [];
|
||||
const blocks = findContinuousTaskBlocks(emptyTasks);
|
||||
expect(blocks).toEqual([]);
|
||||
});
|
||||
|
||||
it("对于乱序输入的任务应正确排序并分组", () => {
|
||||
// 创建乱序排列的任务
|
||||
const mockTasks: SortableTask[] = [
|
||||
createMockTask(5), // 第二个块
|
||||
createMockTask(1), // 第一个块
|
||||
createMockTask(6), // 第二个块
|
||||
createMockTask(0), // 第一个块
|
||||
createMockTask(2), // 第一个块
|
||||
];
|
||||
|
||||
// 执行函数
|
||||
const blocks = findContinuousTaskBlocks(mockTasks);
|
||||
|
||||
// 验证结果 - 应该先排序,然后分成两个块
|
||||
expect(blocks.length).toBe(2);
|
||||
|
||||
// 第一个块(0,1,2)
|
||||
expect(blocks[0].length).toBe(3);
|
||||
expect(blocks[0].map((t) => t.lineNumber).sort()).toEqual([0, 1, 2]);
|
||||
|
||||
// 第二个块(5,6)
|
||||
expect(blocks[1].length).toBe(2);
|
||||
expect(blocks[1].map((t) => t.lineNumber).sort()).toEqual([5, 6]);
|
||||
});
|
||||
});
|
||||
397
src/__tests__/mockUtils.ts
Normal file
397
src/__tests__/mockUtils.ts
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
import { App } from "obsidian";
|
||||
import {
|
||||
Text,
|
||||
Transaction,
|
||||
TransactionSpec,
|
||||
EditorState,
|
||||
ChangeSet,
|
||||
Annotation,
|
||||
EditorSelection,
|
||||
AnnotationType,
|
||||
} from "@codemirror/state";
|
||||
import TaskProgressBarPlugin from "../index"; // Adjust the import path as necessary
|
||||
import {
|
||||
taskStatusChangeAnnotation, // Import the actual annotation
|
||||
} from "../editor-ext/autoCompleteParent"; // Adjust the import path as necessary
|
||||
import { TaskProgressBarSettings } from "../common/setting-definition";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
|
||||
const mockAnnotationType = {
|
||||
of: jest.fn().mockImplementation((value: string) => ({
|
||||
type: mockAnnotationType,
|
||||
value,
|
||||
})),
|
||||
};
|
||||
// Use the actual annotation object from the source file for checks
|
||||
const mockParentTaskStatusChangeAnnotation = taskStatusChangeAnnotation;
|
||||
|
||||
// Mock Text Object - Consolidated version
|
||||
export const createMockText = (content: string): Text => {
|
||||
const lines = content.split("\n");
|
||||
const doc = {
|
||||
toString: () => content,
|
||||
length: content.length,
|
||||
lines: lines.length,
|
||||
line: jest.fn((lineNum: number) => {
|
||||
if (lineNum < 1 || lineNum > lines.length) {
|
||||
throw new Error(
|
||||
`Line ${lineNum} out of range (1-${lines.length})`
|
||||
);
|
||||
}
|
||||
const text = lines[lineNum - 1];
|
||||
let from = 0;
|
||||
for (let i = 0; i < lineNum - 1; i++) {
|
||||
from += lines[i].length + 1; // +1 for newline
|
||||
}
|
||||
return {
|
||||
text: text,
|
||||
from,
|
||||
to: from + text.length,
|
||||
number: lineNum,
|
||||
length: text.length,
|
||||
};
|
||||
}),
|
||||
lineAt: jest.fn((pos: number) => {
|
||||
// Ensure pos is within valid range
|
||||
pos = Math.max(0, Math.min(pos, content.length));
|
||||
let currentPos = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const lineLength = lines[i].length;
|
||||
const lineStart = currentPos;
|
||||
const lineEnd = currentPos + lineLength;
|
||||
// Check if pos is within the current line or at the very end of the document
|
||||
if (pos >= lineStart && pos <= lineEnd) {
|
||||
return {
|
||||
text: lines[i],
|
||||
from: lineStart,
|
||||
to: lineEnd,
|
||||
number: i + 1,
|
||||
length: lineLength,
|
||||
};
|
||||
}
|
||||
currentPos += lineLength + 1; // +1 for newline
|
||||
}
|
||||
// Handle edge case: position at the very end of the file after the last newline
|
||||
if (
|
||||
pos === content.length &&
|
||||
lines.length > 0 &&
|
||||
content.endsWith("\n")
|
||||
) {
|
||||
const lastLineIndex = lines.length - 1;
|
||||
const lastLine = lines[lastLineIndex];
|
||||
let from = content.length - lastLine.length - 1; // Position after the last newline
|
||||
return {
|
||||
text: lastLine,
|
||||
from: from,
|
||||
to: from + lastLine.length,
|
||||
number: lines.length,
|
||||
length: lastLine.length,
|
||||
};
|
||||
} else if (
|
||||
pos === content.length &&
|
||||
lines.length > 0 &&
|
||||
!content.endsWith("\n")
|
||||
) {
|
||||
// Position exactly at the end of the last line (no trailing newline)
|
||||
const lastLineIndex = lines.length - 1;
|
||||
const lastLine = lines[lastLineIndex];
|
||||
let from = 0;
|
||||
for (let i = 0; i < lastLineIndex; i++) {
|
||||
from += lines[i].length + 1;
|
||||
}
|
||||
return {
|
||||
text: lastLine,
|
||||
from: from,
|
||||
to: from + lastLine.length,
|
||||
number: lines.length,
|
||||
length: lastLine.length,
|
||||
};
|
||||
}
|
||||
// If the content is empty or pos is 0 in an empty doc
|
||||
if (content === "" && pos === 0) {
|
||||
return {
|
||||
text: "",
|
||||
from: 0,
|
||||
to: 0,
|
||||
number: 1,
|
||||
length: 0,
|
||||
};
|
||||
}
|
||||
throw new Error(
|
||||
`Could not find line at pos ${pos} in content: "${content}"`
|
||||
);
|
||||
}),
|
||||
sliceString: jest.fn((from: number, to: number) =>
|
||||
content.slice(from, to)
|
||||
),
|
||||
};
|
||||
// @ts-ignore - Add self-reference for Text methods if necessary
|
||||
doc.doc = doc;
|
||||
return doc as Text;
|
||||
};
|
||||
|
||||
// Mock ChangeSet - Consolidated version
|
||||
const createMockChangeSet = (doc: Text, changes: any[] = []): ChangeSet => {
|
||||
return {
|
||||
length: doc.length,
|
||||
// @ts-ignore
|
||||
iterChanges: jest.fn(
|
||||
(
|
||||
callback: (
|
||||
fromA: number,
|
||||
toA: number,
|
||||
fromB: number,
|
||||
toB: number,
|
||||
inserted: Text
|
||||
) => void
|
||||
) => {
|
||||
changes.forEach((change) => {
|
||||
// Basic validation to prevent errors on undefined values
|
||||
const fromA = change.fromA ?? 0;
|
||||
const toA = change.toA ?? fromA;
|
||||
const fromB = change.fromB ?? 0;
|
||||
const insertedText = change.insertedText ?? "";
|
||||
const toB = change.toB ?? fromB + insertedText.length;
|
||||
callback(
|
||||
fromA,
|
||||
toA,
|
||||
fromB,
|
||||
toB,
|
||||
createMockText(insertedText) // inserted text needs to be a Text object
|
||||
);
|
||||
});
|
||||
}
|
||||
),
|
||||
// Add other necessary ChangeSet methods if needed, even if mocked simply
|
||||
// @ts-ignore
|
||||
mapDesc: jest.fn(() => ({
|
||||
/* mock */
|
||||
})),
|
||||
// @ts-ignore
|
||||
compose: jest.fn(() => ({
|
||||
/* mock */
|
||||
})),
|
||||
// @ts-ignore
|
||||
mapPos: jest.fn(() => 0),
|
||||
// @ts-ignore
|
||||
toJSON: jest.fn(() => ({
|
||||
/* mock */
|
||||
})),
|
||||
// @ts-ignore
|
||||
any: jest.fn(() => false),
|
||||
// @ts-ignore
|
||||
get desc() {
|
||||
return {
|
||||
/* mock */
|
||||
};
|
||||
},
|
||||
// @ts-ignore
|
||||
get empty() {
|
||||
return changes.length === 0;
|
||||
},
|
||||
// ... and potentially others like 'apply', 'invert', etc. if used
|
||||
} as unknown as ChangeSet;
|
||||
};
|
||||
|
||||
// Mock Transaction Object - Consolidated version
|
||||
const createMockTransaction = (options: {
|
||||
startStateDocContent?: string;
|
||||
newDocContent?: string;
|
||||
changes?: {
|
||||
fromA: number;
|
||||
toA: number;
|
||||
fromB: number;
|
||||
toB: number;
|
||||
insertedText?: string;
|
||||
}[];
|
||||
docChanged?: boolean;
|
||||
isUserEvent?: string | false; // e.g., 'input.paste' or false
|
||||
annotations?: { type: AnnotationType<any>; value: any }[]; // Use Annotation instead of AnnotationType
|
||||
selection?: { anchor: number; head: number };
|
||||
}): Transaction => {
|
||||
const startDoc = createMockText(options.startStateDocContent ?? "");
|
||||
const newDoc = createMockText(
|
||||
options.newDocContent ?? options.startStateDocContent ?? ""
|
||||
);
|
||||
// Ensure changes array exists and is valid
|
||||
const validChanges =
|
||||
options.changes?.map((c) => ({
|
||||
fromA: c.fromA ?? 0,
|
||||
toA: c.toA ?? c.fromA ?? 0,
|
||||
fromB: c.fromB ?? 0,
|
||||
insertedText: c.insertedText ?? "",
|
||||
toB: c.toB ?? (c.fromB ?? 0) + (c.insertedText ?? "").length,
|
||||
})) || [];
|
||||
const changeSet = createMockChangeSet(newDoc, validChanges);
|
||||
|
||||
// Create a proper EditorSelection object instead of just using an anchor/head object
|
||||
const selectionObj = options.selection || { anchor: 0, head: 0 };
|
||||
const editorSelection = EditorSelection.single(
|
||||
selectionObj.anchor,
|
||||
selectionObj.head
|
||||
); // Use EditorSelection.single for proper creation
|
||||
|
||||
const mockTr = {
|
||||
newDoc: newDoc,
|
||||
changes: changeSet,
|
||||
docChanged:
|
||||
options.docChanged !== undefined
|
||||
? options.docChanged
|
||||
: !!validChanges.length,
|
||||
isUserEvent: jest.fn((type: string) => {
|
||||
if (options.isUserEvent === false) return false;
|
||||
return options.isUserEvent === type;
|
||||
}),
|
||||
annotation: jest.fn(<T>(type: AnnotationType<T>): T | undefined => {
|
||||
const found = options.annotations?.find((ann) => ann.type === type);
|
||||
return found ? found.value : undefined;
|
||||
}),
|
||||
selection: editorSelection,
|
||||
// Add required Transaction properties with basic mocks
|
||||
effects: [],
|
||||
scrollIntoView: false,
|
||||
newSelection: editorSelection,
|
||||
state: {
|
||||
doc: newDoc,
|
||||
selection: editorSelection,
|
||||
// Add other required state properties with basic mocks
|
||||
facet: jest.fn(() => null),
|
||||
field: jest.fn(() => null),
|
||||
fieldInvalidated: jest.fn(() => false),
|
||||
toJSON: jest.fn(() => ({})),
|
||||
replaceSelection: jest.fn(),
|
||||
changeByRange: jest.fn(),
|
||||
changes: jest.fn(),
|
||||
toText: jest.fn(() => newDoc),
|
||||
// @ts-ignore
|
||||
values: [],
|
||||
// @ts-ignore
|
||||
apply: jest.fn(() => ({})),
|
||||
// @ts-ignore
|
||||
update: jest.fn(() => ({})),
|
||||
// @ts-ignore
|
||||
sliceDoc: jest.fn(() => ""),
|
||||
} as unknown as EditorState,
|
||||
startState: EditorState.create({ doc: startDoc }),
|
||||
reconfigured: false,
|
||||
};
|
||||
|
||||
return mockTr as unknown as Transaction;
|
||||
};
|
||||
|
||||
// Mock App Object - Consolidated version
|
||||
const createMockApp = (): App => {
|
||||
const app = new App();
|
||||
// Add any other necessary App mocks here if needed for tests
|
||||
return app;
|
||||
};
|
||||
|
||||
// Mock Plugin Object - Consolidated version with merged settings
|
||||
const createMockPlugin = (
|
||||
settings: Partial<TaskProgressBarSettings> = {} // Use TaskProgressBarSettings directly
|
||||
): TaskProgressBarPlugin => {
|
||||
const defaults: Partial<TaskProgressBarSettings> = {
|
||||
// Default settings from both original versions combined
|
||||
markParentInProgressWhenPartiallyComplete: true,
|
||||
taskStatuses: {
|
||||
inProgress: "/",
|
||||
completed: "x|X",
|
||||
abandoned: "-",
|
||||
planned: "?",
|
||||
notStarted: " ",
|
||||
},
|
||||
taskStatusCycle: ["TODO", "IN_PROGRESS", "DONE"],
|
||||
taskStatusMarks: { TODO: " ", IN_PROGRESS: "/", DONE: "x" },
|
||||
excludeMarksFromCycle: [],
|
||||
workflow: {
|
||||
enableWorkflow: false,
|
||||
autoRemoveLastStageMarker: true,
|
||||
autoAddTimestamp: false,
|
||||
timestampFormat: "YYYY-MM-DD HH:mm:ss",
|
||||
removeTimestampOnTransition: false,
|
||||
calculateSpentTime: false,
|
||||
spentTimeFormat: "HH:mm",
|
||||
definitions: [],
|
||||
autoAddNextTask: false,
|
||||
calculateFullSpentTime: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Deep merge provided settings with defaults
|
||||
// Basic deep merge - might need a library for complex nested objects if issues arise
|
||||
const mergedSettings = {
|
||||
...defaults,
|
||||
...settings,
|
||||
taskStatuses: { ...defaults.taskStatuses, ...settings.taskStatuses },
|
||||
taskStatusMarks: {
|
||||
...defaults.taskStatusMarks,
|
||||
...settings.taskStatusMarks,
|
||||
},
|
||||
workflow: { ...defaults.workflow, ...settings.workflow },
|
||||
};
|
||||
|
||||
// Return the plugin with app property
|
||||
return {
|
||||
settings: mergedSettings as TaskProgressBarSettings,
|
||||
app: createMockApp(), // Use the consolidated mock app
|
||||
} as TaskProgressBarPlugin;
|
||||
};
|
||||
|
||||
// Mock EditorView Object
|
||||
const createMockEditorView = (docContent: string): EditorView => {
|
||||
const doc = createMockText(docContent);
|
||||
const mockState = {
|
||||
doc: doc,
|
||||
// Add other minimal required EditorState properties/methods if needed by the tests
|
||||
// For sortTasks, primarily 'doc' is accessed via view.state.doc
|
||||
facet: jest.fn(() => []),
|
||||
field: jest.fn(() => undefined),
|
||||
fieldInvalidated: jest.fn(() => false),
|
||||
toJSON: jest.fn(() => ({})),
|
||||
replaceSelection: jest.fn(),
|
||||
changeByRange: jest.fn(),
|
||||
changes: jest.fn(() => ({
|
||||
/* mock ChangeSet */
|
||||
})),
|
||||
toText: jest.fn(() => doc),
|
||||
sliceDoc: jest.fn((from = 0, to = doc.length) =>
|
||||
doc.sliceString(from, to)
|
||||
),
|
||||
// @ts-ignore
|
||||
values: [],
|
||||
// @ts-ignore
|
||||
apply: jest.fn((tr: any) => mockState), // Return the same state for simplicity
|
||||
// @ts-ignore
|
||||
update: jest.fn((spec: any) => ({
|
||||
state: mockState,
|
||||
transactions: [],
|
||||
})), // Basic update mock
|
||||
// @ts-ignore
|
||||
selection: {
|
||||
ranges: [{ from: 0, to: 0 }],
|
||||
mainIndex: 0,
|
||||
main: { from: 0, to: 0 },
|
||||
}, // Minimal selection mock
|
||||
} as unknown as EditorState;
|
||||
|
||||
const mockView = {
|
||||
state: mockState,
|
||||
dispatch: jest.fn(), // Mock dispatch function
|
||||
// Add other EditorView properties/methods if needed by tests
|
||||
// For example, if viewport information is accessed
|
||||
// viewport: { from: 0, to: doc.length },
|
||||
// contentDOM: document.createElement('div'), // Basic DOM element mock
|
||||
} as unknown as EditorView;
|
||||
|
||||
return mockView;
|
||||
};
|
||||
|
||||
export {
|
||||
// createMockText is already exported inline
|
||||
createMockChangeSet, // Export the consolidated function
|
||||
createMockTransaction, // Export the consolidated function
|
||||
createMockApp, // Export the consolidated function
|
||||
createMockPlugin, // Export the consolidated function
|
||||
mockParentTaskStatusChangeAnnotation,
|
||||
createMockEditorView, // Export the new function
|
||||
};
|
||||
306
src/__tests__/sortTasks.test.ts
Normal file
306
src/__tests__/sortTasks.test.ts
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
import {
|
||||
sortTasksInDocument,
|
||||
SortCriterion,
|
||||
} from "../commands/sortTaskCommands";
|
||||
import {
|
||||
createMockText,
|
||||
createMockPlugin,
|
||||
createMockEditorView,
|
||||
} from "./mockUtils";
|
||||
|
||||
describe("sortTasksInDocument", () => {
|
||||
// Reset Notice mock after each test
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should identify and sort tasks", () => {
|
||||
// Original content: mixed task order
|
||||
const originalContent = `
|
||||
- [ ] Incomplete task 1
|
||||
- [x] Completed task
|
||||
- [/] In progress task`;
|
||||
|
||||
// Create mock EditorView and plugin
|
||||
const mockView = createMockEditorView(originalContent);
|
||||
const mockPlugin = createMockPlugin();
|
||||
|
||||
// Sort criteria: by status (incomplete -> in progress -> completed)
|
||||
const sortCriteria: SortCriterion[] = [
|
||||
{ field: "status", order: "asc" },
|
||||
];
|
||||
|
||||
// Call sort function
|
||||
const result = sortTasksInDocument(
|
||||
mockView,
|
||||
mockPlugin,
|
||||
sortCriteria,
|
||||
true
|
||||
);
|
||||
|
||||
// Expected result: text sorted by status
|
||||
const expectedContent = `
|
||||
- [ ] Incomplete task 1
|
||||
- [/] In progress task
|
||||
- [x] Completed task`;
|
||||
|
||||
// Verify sort result
|
||||
expect(result).toEqual(expectedContent);
|
||||
});
|
||||
|
||||
it("should place completed tasks at the end regardless of sort criteria", () => {
|
||||
// Original content: mixed task order
|
||||
const originalContent = `
|
||||
- [x] Completed task 1
|
||||
- [ ] Incomplete task [priority:: high] [due:: 2025-05-01]
|
||||
- [/] In progress task [start:: 2025-04-01]
|
||||
- [x] Completed task 2`;
|
||||
|
||||
// Create mock EditorView and plugin
|
||||
const mockView = createMockEditorView(originalContent);
|
||||
const mockPlugin = createMockPlugin();
|
||||
|
||||
// Sort criteria: by priority (which would normally put tasks with priority first)
|
||||
const sortCriteria: SortCriterion[] = [
|
||||
{ field: "priority", order: "asc" },
|
||||
];
|
||||
|
||||
// Call sort function
|
||||
const result = sortTasksInDocument(
|
||||
mockView,
|
||||
mockPlugin,
|
||||
sortCriteria,
|
||||
true
|
||||
);
|
||||
|
||||
// Expected result: incomplete tasks sorted by priority, but completed tasks always at the end
|
||||
const expectedContent = `
|
||||
- [ ] Incomplete task [priority:: high] [due:: 2025-05-01]
|
||||
- [/] In progress task [start:: 2025-04-01]
|
||||
- [x] Completed task 1
|
||||
- [x] Completed task 2`;
|
||||
|
||||
// Verify sort result
|
||||
expect(result).toEqual(expectedContent);
|
||||
});
|
||||
|
||||
it("should maintain relative position of non-contiguous task blocks", () => {
|
||||
// Original content: two task blocks separated by non-task lines
|
||||
const originalContent = `
|
||||
First task block:
|
||||
- [x] Completed task 1
|
||||
- [ ] Incomplete task 1
|
||||
|
||||
Middle non-task content
|
||||
|
||||
Second task block:
|
||||
- [x] Completed task 2
|
||||
- [ ] Incomplete task 2`;
|
||||
|
||||
// Create mock EditorView and plugin
|
||||
const mockView = createMockEditorView(originalContent);
|
||||
const mockPlugin = createMockPlugin();
|
||||
|
||||
// Sort criteria: by status
|
||||
const sortCriteria: SortCriterion[] = [
|
||||
{ field: "status", order: "asc" },
|
||||
];
|
||||
|
||||
// Call sort function
|
||||
const result = sortTasksInDocument(
|
||||
mockView,
|
||||
mockPlugin,
|
||||
sortCriteria,
|
||||
true
|
||||
);
|
||||
|
||||
// Expected result: each block sorted internally, but blocks maintain relative position
|
||||
const expectedContent = `
|
||||
First task block:
|
||||
- [ ] Incomplete task 1
|
||||
- [x] Completed task 1
|
||||
|
||||
Middle non-task content
|
||||
|
||||
Second task block:
|
||||
- [ ] Incomplete task 2
|
||||
- [x] Completed task 2`;
|
||||
|
||||
// Verify sort result
|
||||
expect(result).toEqual(expectedContent);
|
||||
});
|
||||
|
||||
it("should preserve task hierarchy (parent-child relationships)", () => {
|
||||
// Original content: tasks with parent-child relationships
|
||||
const originalContent = `
|
||||
- [x] Parent task 1
|
||||
- [ ] Child task 1
|
||||
- [/] Child task 2
|
||||
- [ ] Parent task 2
|
||||
- [x] Child task 3`;
|
||||
|
||||
// Create mock EditorView and plugin
|
||||
const mockView = createMockEditorView(originalContent);
|
||||
const mockPlugin = createMockPlugin();
|
||||
|
||||
// Sort criteria: by status
|
||||
const sortCriteria: SortCriterion[] = [
|
||||
{ field: "status", order: "asc" },
|
||||
];
|
||||
|
||||
// Call sort function
|
||||
const result = sortTasksInDocument(
|
||||
mockView,
|
||||
mockPlugin,
|
||||
sortCriteria,
|
||||
true
|
||||
);
|
||||
|
||||
// Expected result: parent tasks sorted, child tasks follow their respective parents
|
||||
const expectedContent = `
|
||||
- [ ] Parent task 2
|
||||
- [x] Child task 3
|
||||
- [x] Parent task 1
|
||||
- [ ] Child task 1
|
||||
- [/] Child task 2`;
|
||||
|
||||
// Verify sort result
|
||||
expect(result).toEqual(expectedContent);
|
||||
});
|
||||
|
||||
it("should sort tasks by multiple criteria", () => {
|
||||
// Original content: tasks with various metadata
|
||||
const originalContent = `
|
||||
- [ ] Low priority [priority:: low] [due:: 2025-05-01]
|
||||
- [ ] High priority [priority:: high]
|
||||
- [ ] Medium priority with due date [priority:: medium] [due:: 2025-04-01]
|
||||
- [ ] Medium priority with later due date [priority:: medium] [due:: 2025-06-01]`;
|
||||
|
||||
// Create mock EditorView and plugin
|
||||
const mockView = createMockEditorView(originalContent);
|
||||
const mockPlugin = createMockPlugin({
|
||||
preferMetadataFormat: "dataview",
|
||||
});
|
||||
|
||||
// Sort criteria: first by priority, then by due date
|
||||
const sortCriteria: SortCriterion[] = [
|
||||
{ field: "priority", order: "asc" },
|
||||
{ field: "dueDate", order: "asc" },
|
||||
];
|
||||
|
||||
// Call sort function
|
||||
const result = sortTasksInDocument(
|
||||
mockView,
|
||||
mockPlugin,
|
||||
sortCriteria,
|
||||
true
|
||||
);
|
||||
|
||||
// Expected result: sorted first by priority (high->medium->low), then by due date (early->late)
|
||||
const expectedContent = `
|
||||
- [ ] High priority [priority:: high]
|
||||
- [ ] Medium priority with due date [priority:: medium] [due:: 2025-04-01]
|
||||
- [ ] Medium priority with later due date [priority:: medium] [due:: 2025-06-01]
|
||||
- [ ] Low priority [priority:: low] [due:: 2025-05-01]`;
|
||||
|
||||
// Verify sort result
|
||||
expect(result).toEqual(expectedContent);
|
||||
});
|
||||
|
||||
it("should return null when there are no tasks to sort", () => {
|
||||
// Original content: no tasks
|
||||
const originalContent = `
|
||||
This is a document with no tasks
|
||||
Just regular text content`;
|
||||
|
||||
// Create mock EditorView and plugin
|
||||
const mockView = createMockEditorView(originalContent);
|
||||
const mockPlugin = createMockPlugin();
|
||||
|
||||
// Sort criteria
|
||||
const sortCriteria: SortCriterion[] = [
|
||||
{ field: "status", order: "asc" },
|
||||
];
|
||||
|
||||
// Call sort function
|
||||
const result = sortTasksInDocument(
|
||||
mockView,
|
||||
mockPlugin,
|
||||
sortCriteria,
|
||||
true
|
||||
);
|
||||
|
||||
// Verify result is null
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should correctly sort tasks with dataview inline fields", () => {
|
||||
// Original content: tasks with dataview inline fields
|
||||
const originalContent = `
|
||||
- [ ] Task B [priority:: low]
|
||||
- [ ] Task A [priority:: high]
|
||||
- [x] Completed Task C`;
|
||||
|
||||
// Create mock EditorView and plugin with dataview enabled
|
||||
const mockView = createMockEditorView(originalContent);
|
||||
const mockPlugin = createMockPlugin({
|
||||
preferMetadataFormat: "dataview",
|
||||
});
|
||||
|
||||
// Sort criteria: by priority
|
||||
const sortCriteria: SortCriterion[] = [
|
||||
{ field: "priority", order: "asc" },
|
||||
];
|
||||
|
||||
// Call sort function
|
||||
const result = sortTasksInDocument(
|
||||
mockView,
|
||||
mockPlugin,
|
||||
sortCriteria,
|
||||
true
|
||||
);
|
||||
|
||||
// Expected result: sorted by priority, completed task at the end
|
||||
const expectedContent = `
|
||||
- [ ] Task A [priority:: high]
|
||||
- [ ] Task B [priority:: low]
|
||||
- [x] Completed Task C`;
|
||||
|
||||
// Verify sort result
|
||||
expect(result).toEqual(expectedContent);
|
||||
});
|
||||
|
||||
it("should correctly sort tasks with Tasks plugin emojis", () => {
|
||||
// Original content: tasks with Tasks plugin emojis
|
||||
const originalContent = `
|
||||
- [ ] Task C 📅 2025-01-03
|
||||
- [ ] Task A 📅 2025-01-01
|
||||
- [x] Completed Task B 📅 2025-01-02`;
|
||||
|
||||
// Create mock EditorView and plugin with tasks plugin enabled
|
||||
const mockView = createMockEditorView(originalContent);
|
||||
const mockPlugin = createMockPlugin({ preferMetadataFormat: "tasks" });
|
||||
|
||||
// Sort criteria: by due date
|
||||
const sortCriteria: SortCriterion[] = [
|
||||
{ field: "dueDate", order: "asc" },
|
||||
];
|
||||
|
||||
// Call sort function
|
||||
const result = sortTasksInDocument(
|
||||
mockView,
|
||||
mockPlugin,
|
||||
sortCriteria,
|
||||
true
|
||||
);
|
||||
|
||||
// Expected result: sorted by due date, completed task at the end
|
||||
const expectedContent = `
|
||||
- [ ] Task A 📅 2025-01-01
|
||||
- [ ] Task C 📅 2025-01-03
|
||||
- [x] Completed Task B 📅 2025-01-02`;
|
||||
|
||||
// Verify sort result
|
||||
expect(result).toEqual(expectedContent);
|
||||
});
|
||||
});
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
MarkdownFileInfo,
|
||||
moment,
|
||||
} from "obsidian";
|
||||
import TaskProgressBarPlugin from "..";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import { buildIndentString, getTabSize } from "../utils";
|
||||
import { t } from "../translations/helper";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,12 @@
|
|||
import { ChangeSpec } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { Task as IndexerTask } from "../utils/types/TaskIndex"; // Import Task type from worker definitions
|
||||
import { parseLocalDate } from "../utils/dateUtil";
|
||||
import {
|
||||
TASK_REGEX,
|
||||
EMOJI_PRIORITY_REGEX,
|
||||
EMOJI_DUE_DATE_REGEX,
|
||||
DV_PRIORITY_REGEX,
|
||||
DV_DUE_DATE_REGEX,
|
||||
} from "../common/regex-define";
|
||||
import { PRIORITY_MAP } from "../common/default-symbol";
|
||||
import TaskProgressBarPlugin from ".."; // Import the plugin class
|
||||
import { TaskProgressBarSettings } from "../common/setting-definition"; // Import settings type
|
||||
import { TFile, App, MetadataCache, Notice } from "obsidian"; // Added Notice
|
||||
import { Notice } from "obsidian";
|
||||
import { parseTaskLine, MetadataFormat } from "../utils/taskUtil";
|
||||
import { Task as IndexerTask } from "../utils/types/TaskIndex";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import { TaskProgressBarSettings } from "../common/setting-definition";
|
||||
|
||||
// Task statuses (aligned with common usage and sorting needs)
|
||||
enum SortableTaskStatus {
|
||||
export enum SortableTaskStatus {
|
||||
Overdue = "overdue", // Calculated, not a raw status
|
||||
DueSoon = "due_soon", // Calculated, not a raw status - Placeholder
|
||||
InProgress = "/",
|
||||
|
|
@ -30,14 +21,18 @@ enum SortableTaskStatus {
|
|||
|
||||
// Interface for tasks used within the sorting command, closely matching IndexerTask
|
||||
// We add calculated fields needed for sorting
|
||||
interface SortableTask extends Omit<IndexerTask, "id" | "children" | "parent"> {
|
||||
id: string; // Keep ID for potential mapping/diffing
|
||||
lineNumber: number; // Original line number (0-based)
|
||||
indentation: number; // Calculated indentation level
|
||||
children: SortableTask[]; // Children of the same type
|
||||
parent?: SortableTask; // Parent reference
|
||||
calculatedStatus: SortableTaskStatus | string; // Status used for sorting (can include 'overdue')
|
||||
// Inherited from IndexerTask:
|
||||
export interface SortableTask
|
||||
extends Omit<
|
||||
IndexerTask,
|
||||
"id" | "children" | "parent" | "filePath" | "line"
|
||||
> {
|
||||
id: string; // Use generated ID like line-${lineNumber} or keep parsed one? Let's keep parsed one.
|
||||
lineNumber: number; // 0-based, relative to document start
|
||||
indentation: number;
|
||||
children: SortableTask[];
|
||||
parent?: SortableTask;
|
||||
calculatedStatus: SortableTaskStatus | string; // Status used for sorting
|
||||
// Fields mapped from parsed Task
|
||||
originalMarkdown: string;
|
||||
status: string;
|
||||
completed: boolean;
|
||||
|
|
@ -46,7 +41,14 @@ interface SortableTask extends Omit<IndexerTask, "id" | "children" | "parent"> {
|
|||
dueDate?: number;
|
||||
startDate?: number;
|
||||
scheduledDate?: number;
|
||||
// Add other inherited fields if needed by sorting or parsing
|
||||
tags: string[]; // Changed from tags? to tags to match base Task interface
|
||||
// Add any other fields from IndexerTask if needed by sorting criteria
|
||||
// createdDate?: number;
|
||||
// completedDate?: number;
|
||||
// recurrence?: string;
|
||||
// project?: string;
|
||||
// context?: string;
|
||||
// tags?: string[]; // Keep tags if needed for sorting/filtering later? Not currently used.
|
||||
}
|
||||
|
||||
// Sorting criteria
|
||||
|
|
@ -69,122 +71,86 @@ function getIndentationLevel(line: string): number {
|
|||
return match[1].length;
|
||||
}
|
||||
|
||||
// Modified: Parses tasks from a text block with a line offset
|
||||
// --- Refactored Task Parsing using taskUtil ---
|
||||
function parseTasksForSorting(
|
||||
blockText: string,
|
||||
lineOffset: number = 0
|
||||
lineOffset: number = 0,
|
||||
filePath: string, // Added filePath
|
||||
format: MetadataFormat // Added format
|
||||
): SortableTask[] {
|
||||
const lines = blockText.split("\n");
|
||||
const tasks: SortableTask[] = [];
|
||||
// taskMap uses the absolute line number as key
|
||||
const taskMap: { [lineNumber: number]: SortableTask } = {};
|
||||
let currentParentStack: SortableTask[] = [];
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const match = line.match(TASK_REGEX);
|
||||
if (match) {
|
||||
const indentationStr = match[1];
|
||||
const status = match[3];
|
||||
let contentWithMetadata = match[4];
|
||||
const lineNumber = lineOffset + index; // Calculate absolute line number
|
||||
const lineNumber = lineOffset + index; // Calculate absolute line number (0-based)
|
||||
|
||||
// Use the robust parser from taskUtil
|
||||
// Note: parseTaskLine expects 1-based line number for ID generation, pass lineNumber + 1
|
||||
const parsedTask = parseTaskLine(
|
||||
filePath,
|
||||
line,
|
||||
lineNumber + 1,
|
||||
format
|
||||
); // Pass 1-based line number
|
||||
|
||||
if (parsedTask) {
|
||||
// We have a valid task line, now map it to SortableTask
|
||||
const indentation = getIndentationLevel(line);
|
||||
|
||||
const newTask: Partial<SortableTask> = {
|
||||
id: `line-${lineNumber}`,
|
||||
lineNumber: lineNumber,
|
||||
originalMarkdown: line, // Store original line from the block
|
||||
status: status,
|
||||
completed: status.toLowerCase() === "x",
|
||||
indentation: indentation,
|
||||
children: [],
|
||||
};
|
||||
|
||||
// --- Metadata Extraction (Simplified) ---
|
||||
let remainingContent = contentWithMetadata;
|
||||
let priorityValue: number | undefined = undefined;
|
||||
let priorityMatch = remainingContent.match(DV_PRIORITY_REGEX);
|
||||
if (priorityMatch?.[1]) {
|
||||
const prioStr = priorityMatch[1].trim().toLowerCase();
|
||||
priorityValue = PRIORITY_MAP[prioStr] ?? parseInt(prioStr, 10);
|
||||
if (!isNaN(priorityValue)) {
|
||||
remainingContent = remainingContent.replace(
|
||||
priorityMatch[0],
|
||||
""
|
||||
);
|
||||
} else {
|
||||
priorityValue = undefined;
|
||||
}
|
||||
}
|
||||
if (priorityValue === undefined) {
|
||||
priorityMatch = remainingContent.match(EMOJI_PRIORITY_REGEX);
|
||||
if (priorityMatch?.[1]) {
|
||||
priorityValue = PRIORITY_MAP[priorityMatch[1]];
|
||||
if (priorityValue !== undefined) {
|
||||
remainingContent = remainingContent.replace(
|
||||
priorityMatch[0],
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
newTask.priority = priorityValue;
|
||||
|
||||
let dueDateTimestamp: number | undefined = undefined;
|
||||
let dateMatch = remainingContent.match(DV_DUE_DATE_REGEX);
|
||||
if (dateMatch?.[1]) {
|
||||
dueDateTimestamp = parseLocalDate(dateMatch[1]);
|
||||
if (dueDateTimestamp !== undefined) {
|
||||
remainingContent = remainingContent.replace(
|
||||
dateMatch[0],
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
if (dueDateTimestamp === undefined) {
|
||||
dateMatch = remainingContent.match(EMOJI_DUE_DATE_REGEX);
|
||||
if (dateMatch?.[1]) {
|
||||
dueDateTimestamp = parseLocalDate(dateMatch[1]);
|
||||
if (dueDateTimestamp !== undefined) {
|
||||
remainingContent = remainingContent.replace(
|
||||
dateMatch[0],
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
newTask.dueDate = dueDateTimestamp;
|
||||
|
||||
// --- Calculated Status ---
|
||||
let calculatedStatus: SortableTaskStatus | string = status;
|
||||
// --- Calculate Sortable Status ---
|
||||
let calculatedStatus: SortableTaskStatus | string =
|
||||
parsedTask.status;
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
const todayTimestamp = now.getTime();
|
||||
|
||||
if (
|
||||
!newTask.completed &&
|
||||
status !== SortableTaskStatus.Cancelled && // Use enum value
|
||||
newTask.dueDate &&
|
||||
newTask.dueDate < todayTimestamp
|
||||
!parsedTask.completed &&
|
||||
parsedTask.status !== SortableTaskStatus.Cancelled && // Compare against enum
|
||||
parsedTask.dueDate &&
|
||||
parsedTask.dueDate < todayTimestamp
|
||||
) {
|
||||
calculatedStatus = SortableTaskStatus.Overdue; // Use enum value
|
||||
calculatedStatus = SortableTaskStatus.Overdue; // Use enum
|
||||
} else {
|
||||
// Ensure the original status maps to the enum if possible
|
||||
calculatedStatus = Object.values(SortableTaskStatus).includes(
|
||||
status as SortableTaskStatus
|
||||
parsedTask.status as SortableTaskStatus
|
||||
)
|
||||
? (status as SortableTaskStatus)
|
||||
: status;
|
||||
? (parsedTask.status as SortableTaskStatus)
|
||||
: parsedTask.status;
|
||||
}
|
||||
newTask.calculatedStatus = calculatedStatus;
|
||||
|
||||
newTask.content = remainingContent.replace(/\s{2,}/g, " ").trim();
|
||||
// --- Create SortableTask ---
|
||||
const sortableTask: SortableTask = {
|
||||
// Map fields from parsedTask
|
||||
id: parsedTask.id, // Use ID from parser
|
||||
originalMarkdown: parsedTask.originalMarkdown,
|
||||
status: parsedTask.status,
|
||||
completed: parsedTask.completed,
|
||||
content: parsedTask.content,
|
||||
priority: parsedTask.priority,
|
||||
dueDate: parsedTask.dueDate,
|
||||
startDate: parsedTask.startDate,
|
||||
scheduledDate: parsedTask.scheduledDate,
|
||||
tags: parsedTask.tags || [], // Map tags, default to empty array
|
||||
// Fields specific to SortableTask / required for sorting logic
|
||||
lineNumber: lineNumber, // Keep 0-based line number for sorting stability
|
||||
indentation: indentation,
|
||||
children: [],
|
||||
calculatedStatus: calculatedStatus,
|
||||
// parent will be set below
|
||||
};
|
||||
|
||||
// --- Build Hierarchy (relative to the parsed block) ---
|
||||
const completeTask = newTask as SortableTask;
|
||||
taskMap[lineNumber] = completeTask; // Use absolute line number for mapping
|
||||
// --- Build Hierarchy ---
|
||||
taskMap[lineNumber] = sortableTask; // Use 0-based absolute line number
|
||||
|
||||
// Find parent based on indentation
|
||||
while (
|
||||
currentParentStack.length > 0 &&
|
||||
indentation <=
|
||||
indentation <= // Child must have greater indentation than parent
|
||||
currentParentStack[currentParentStack.length - 1]
|
||||
.indentation
|
||||
) {
|
||||
|
|
@ -194,16 +160,16 @@ function parseTasksForSorting(
|
|||
if (currentParentStack.length > 0) {
|
||||
const parent =
|
||||
currentParentStack[currentParentStack.length - 1];
|
||||
parent.children.push(completeTask);
|
||||
completeTask.parent = parent;
|
||||
parent.children.push(sortableTask);
|
||||
sortableTask.parent = parent;
|
||||
} else {
|
||||
tasks.push(completeTask); // Top-level task within the block
|
||||
tasks.push(sortableTask); // Add as top-level task within the block
|
||||
}
|
||||
|
||||
currentParentStack.push(completeTask);
|
||||
currentParentStack.push(sortableTask); // Push current task onto stack
|
||||
} else {
|
||||
// Non-task line in the block
|
||||
// currentParentStack = []; // Resetting might be too aggressive here
|
||||
// Non-task line encountered
|
||||
// Keep the stack, assuming tasks under a non-task might still be related hierarchically.
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -298,7 +264,6 @@ function getDynamicStatusOrder(settings: TaskProgressBarSettings): {
|
|||
if (!(" " in order)) order[" "] = order[" "] ?? 10; // Default incomplete reasonably high
|
||||
if (!("x" in order)) order["x"] = order["x"] ?? 98; // Default complete towards end
|
||||
|
||||
console.debug("Generated Status Order:", order); // For debugging
|
||||
return order;
|
||||
}
|
||||
|
||||
|
|
@ -312,6 +277,12 @@ function compareTasks(
|
|||
// Generate status order dynamically for this comparison run
|
||||
const statusOrder = getDynamicStatusOrder(settings);
|
||||
|
||||
// First, completed tasks always come last
|
||||
if (taskA.completed !== taskB.completed) {
|
||||
return taskA.completed ? 1 : -1;
|
||||
}
|
||||
|
||||
// If both are completed or both incomplete, sort by original criteria
|
||||
for (const criterion of criteria) {
|
||||
let valA: any;
|
||||
let valB: any;
|
||||
|
|
@ -319,73 +290,169 @@ function compareTasks(
|
|||
|
||||
switch (criterion.field) {
|
||||
case "status":
|
||||
valA = statusOrder[taskA.calculatedStatus] ?? 1000; // Unknown goes last
|
||||
// Status comparison logic (relies on statusOrder having numbers)
|
||||
valA = statusOrder[taskA.calculatedStatus] ?? 1000; // Assign a high number for unknown statuses
|
||||
valB = statusOrder[taskB.calculatedStatus] ?? 1000;
|
||||
comparison = valA - valB;
|
||||
if (typeof valA === "number" && typeof valB === "number") {
|
||||
comparison = valA - valB; // Lower number means higher rank in status order
|
||||
} else {
|
||||
// Fallback if statusOrder contains non-numbers (shouldn't happen ideally)
|
||||
console.warn(
|
||||
`Non-numeric status order values detected: ${valA}, ${valB}`
|
||||
);
|
||||
comparison = 0; // Treat as equal if non-numeric
|
||||
}
|
||||
break;
|
||||
case "priority":
|
||||
valA = taskA.priority ?? Infinity; // Tasks without priority go last
|
||||
valB = taskB.priority ?? Infinity;
|
||||
comparison = valA - valB;
|
||||
// Assuming numerical mapping where HIGHER number means HIGHER priority (e.g., High=3, Medium=2, Low=1)
|
||||
valA = taskA.priority; // Use undefined/null directly
|
||||
valB = taskB.priority;
|
||||
const aHasPriority = valA !== undefined && valA !== null;
|
||||
const bHasPriority = valB !== undefined && valB !== null;
|
||||
|
||||
if (!aHasPriority && !bHasPriority) {
|
||||
comparison = 0; // Both lack priority
|
||||
} else if (!aHasPriority) {
|
||||
// A lacks priority. 'asc' means High->Low->None. None is last (+1).
|
||||
comparison = 1;
|
||||
} else if (!bHasPriority) {
|
||||
// B lacks priority. 'asc' means High->Low->None. None is last. B is last, so A is first (-1).
|
||||
comparison = -1;
|
||||
} else {
|
||||
// Both have numeric priorities.
|
||||
// Compare numerically: A - B.
|
||||
// Example: High(3) vs Low(1) => comparison = 3 - 1 = 2
|
||||
comparison = valA - valB;
|
||||
}
|
||||
// Direction adjustment is handled after the switch
|
||||
break;
|
||||
case "dueDate":
|
||||
case "startDate":
|
||||
case "scheduledDate":
|
||||
valA = taskA[criterion.field] ?? Infinity; // No date goes last
|
||||
valB = taskB[criterion.field] ?? Infinity;
|
||||
if (valA === Infinity && valB !== Infinity) comparison = 1;
|
||||
else if (valA !== Infinity && valB === Infinity)
|
||||
valA = taskA[criterion.field]; // Use undefined/null directly
|
||||
valB = taskB[criterion.field];
|
||||
const aHasDate = valA !== undefined && valA !== null;
|
||||
const bHasDate = valB !== undefined && valB !== null;
|
||||
|
||||
if (!aHasDate && !bHasDate) {
|
||||
comparison = 0; // Both lack date
|
||||
} else if (!aHasDate) {
|
||||
// A lacks date. 'asc' means Dates->None. None is last (+1).
|
||||
comparison = 1;
|
||||
} else if (!bHasDate) {
|
||||
// B lacks date. 'asc' means Dates->None. None is last. B is last, so A is first (-1).
|
||||
comparison = -1;
|
||||
else comparison = valA - valB;
|
||||
} else {
|
||||
// Both have numeric dates (timestamps)
|
||||
// Compare numerically: A - B. Earlier date first.
|
||||
comparison = valA - valB;
|
||||
}
|
||||
// Direction adjustment is handled after the switch
|
||||
break;
|
||||
case "content":
|
||||
valA = taskA.content.toLowerCase();
|
||||
valB = taskB.content.toLowerCase();
|
||||
comparison = valA.localeCompare(valB);
|
||||
// Direction adjustment is handled after the switch
|
||||
break;
|
||||
}
|
||||
} // End switch
|
||||
|
||||
// Apply sort order direction (asc/desc)
|
||||
if (comparison !== 0) {
|
||||
return criterion.order === "asc" ? comparison : -comparison;
|
||||
}
|
||||
}
|
||||
// Special handling for priority: 'asc' means HIGHER numeric value first.
|
||||
if (criterion.field === "priority") {
|
||||
// If 'asc' (High first), comparison = A - B. High(3) - Low(1) = 2. We need negative result -> return -comparison.
|
||||
// If 'desc' (Low first), comparison = A - B. High(3) - Low(1) = 2. We need positive result -> return comparison.
|
||||
return criterion.order === "asc" ? -comparison : comparison;
|
||||
} else {
|
||||
// For other fields (status, dates, content): 'asc' means LOWER numeric/alphabetic value first.
|
||||
// Comparison = A - B or localeCompare(A, B).
|
||||
// If 'asc', a negative comparison means A is first -> return comparison.
|
||||
// If 'desc', flip the sign -> return -comparison.
|
||||
return criterion.order === "asc" ? comparison : -comparison;
|
||||
}
|
||||
} // End if comparison !== 0
|
||||
} // End for loop
|
||||
|
||||
// Maintain original relative order if all criteria are equal
|
||||
return taskA.lineNumber - taskB.lineNumber;
|
||||
}
|
||||
|
||||
// Recursively sorts a list of tasks and their children using settings
|
||||
function sortTaskList(
|
||||
tasks: SortableTask[],
|
||||
criteria: SortCriterion[],
|
||||
settings: TaskProgressBarSettings // Pass settings here
|
||||
): SortableTask[] {
|
||||
// Sort tasks at the current level
|
||||
tasks.sort((a, b) => compareTasks(a, b, criteria, settings)); // Pass settings
|
||||
// Find continuous task blocks (including subtasks)
|
||||
export function findContinuousTaskBlocks(
|
||||
tasks: SortableTask[]
|
||||
): SortableTask[][] {
|
||||
if (tasks.length === 0) return [];
|
||||
|
||||
// Recursively sort children
|
||||
tasks.forEach((task) => {
|
||||
if (task.children && task.children.length > 0) {
|
||||
// Pass settings down recursively
|
||||
sortTaskList(task.children, criteria, settings);
|
||||
// Sort by line number
|
||||
const sortedTasks = [...tasks].sort((a, b) => a.lineNumber - b.lineNumber);
|
||||
|
||||
// Task blocks array
|
||||
const blocks: SortableTask[][] = [];
|
||||
let currentBlock: SortableTask[] = [sortedTasks[0]];
|
||||
|
||||
// Recursively find the maximum line number of a task and all its children
|
||||
function getMaxLineNumberWithChildren(task: SortableTask): number {
|
||||
if (!task.children || task.children.length === 0)
|
||||
return task.lineNumber;
|
||||
|
||||
let maxLine = task.lineNumber;
|
||||
for (const child of task.children) {
|
||||
const childMaxLine = getMaxLineNumberWithChildren(child);
|
||||
maxLine = Math.max(maxLine, childMaxLine);
|
||||
}
|
||||
});
|
||||
|
||||
return tasks; // Return the sorted list (though modification is in-place)
|
||||
return maxLine;
|
||||
}
|
||||
|
||||
// Check all tasks, group into continuous blocks
|
||||
for (let i = 1; i < sortedTasks.length; i++) {
|
||||
const prevTask = sortedTasks[i - 1];
|
||||
const currentTask = sortedTasks[i];
|
||||
|
||||
// Check the maximum line number of the previous task (including all subtasks)
|
||||
const prevMaxLine = getMaxLineNumberWithChildren(prevTask);
|
||||
|
||||
// If the current task line number is the next line after the previous task or its subtasks, it belongs to the same block
|
||||
if (currentTask.lineNumber <= prevMaxLine + 1) {
|
||||
currentBlock.push(currentTask);
|
||||
} else {
|
||||
// Otherwise start a new block
|
||||
blocks.push([...currentBlock]);
|
||||
currentBlock = [currentTask];
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last block
|
||||
if (currentBlock.length > 0) {
|
||||
blocks.push(currentBlock);
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// --- 4. Generate Codemirror Changes (Modified) ---
|
||||
// Recursively sort tasks and their subtasks
|
||||
function sortTasksRecursively(
|
||||
tasks: SortableTask[],
|
||||
criteria: SortCriterion[],
|
||||
settings: TaskProgressBarSettings
|
||||
): SortableTask[] {
|
||||
// Sort tasks at the current level
|
||||
tasks.sort((a, b) => compareTasks(a, b, criteria, settings));
|
||||
|
||||
// Flattens a task tree back into text lines using originalMarkdown
|
||||
function flattenTasksToText(tasks: SortableTask[]): string {
|
||||
let lines: string[] = [];
|
||||
tasks.forEach((task) => {
|
||||
lines.push(task.originalMarkdown); // Preserves original formatting/indent
|
||||
// Recursively sort each task's subtasks
|
||||
for (const task of tasks) {
|
||||
if (task.children && task.children.length > 0) {
|
||||
lines.push(flattenTasksToText(task.children));
|
||||
// Ensure sorted subtasks are saved back to task.children
|
||||
task.children = sortTasksRecursively(
|
||||
task.children,
|
||||
criteria,
|
||||
settings
|
||||
);
|
||||
}
|
||||
});
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
return tasks; // Return the sorted task array
|
||||
}
|
||||
|
||||
// Main function: Parses, sorts, and generates Codemirror changes
|
||||
|
|
@ -394,13 +461,14 @@ export function sortTasksInDocument(
|
|||
plugin: TaskProgressBarPlugin,
|
||||
sortCriteria: SortCriterion[],
|
||||
fullDocument: boolean = false // Keep parameter
|
||||
): ChangeSpec[] | null {
|
||||
): string | null {
|
||||
const app = plugin.app;
|
||||
const activeFile = app.workspace.getActiveFile(); // Assume command runs on active file
|
||||
if (!activeFile) {
|
||||
new Notice("Sort Tasks: No active file found.");
|
||||
return null;
|
||||
}
|
||||
const filePath = activeFile.path; // Get file path
|
||||
const cache = app.metadataCache.getFileCache(activeFile);
|
||||
if (!cache) {
|
||||
new Notice("Sort Tasks: Metadata cache not available.");
|
||||
|
|
@ -409,6 +477,7 @@ export function sortTasksInDocument(
|
|||
|
||||
const doc = view.state.doc;
|
||||
const settings = plugin.settings;
|
||||
const metadataFormat: MetadataFormat = plugin.settings.preferMetadataFormat;
|
||||
|
||||
let startLine = 0;
|
||||
let endLine = doc.lines - 1;
|
||||
|
|
@ -465,9 +534,6 @@ export function sortTasksInDocument(
|
|||
const toOffsetOriginal = doc.line(endLine + 1).to;
|
||||
// Ensure offsets are valid
|
||||
if (fromOffsetOriginal > toOffsetOriginal) {
|
||||
console.log(
|
||||
`Sort Tasks: Invalid range calculated (startLine: ${startLine}, endLine: ${endLine}). Aborting.`
|
||||
);
|
||||
new Notice(`Sort Tasks: Invalid range calculated for ${scopeMessage}.`);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -476,111 +542,142 @@ export function sortTasksInDocument(
|
|||
toOffsetOriginal
|
||||
);
|
||||
|
||||
// 1. Parse tasks *only* within the determined block, providing the offset
|
||||
const blockTasks = parseTasksForSorting(originalBlockText, startLine);
|
||||
// 1. Parse tasks *using the new function*, providing offset, path, and format
|
||||
const blockTasks = parseTasksForSorting(
|
||||
originalBlockText,
|
||||
startLine,
|
||||
filePath,
|
||||
metadataFormat // Pass determined format
|
||||
);
|
||||
if (blockTasks.length === 0) {
|
||||
const noticeMsg = `Sort Tasks: No tasks found in the ${scopeMessage} (Lines ${
|
||||
startLine + 1
|
||||
}-${endLine + 1}) to sort.`;
|
||||
console.log(noticeMsg);
|
||||
new Notice(noticeMsg);
|
||||
return null;
|
||||
}
|
||||
console.log("blockTasks", blockTasks);
|
||||
|
||||
// 2. Sort the tasks parsed from the block using plugin settings
|
||||
// sortTaskList modifies the array in-place and sorts children recursively
|
||||
const sortedBlockTasks = sortTaskList(
|
||||
[...blockTasks],
|
||||
sortCriteria,
|
||||
settings
|
||||
); // Pass settings, sort a copy
|
||||
// Find continuous task blocks
|
||||
const taskBlocks = findContinuousTaskBlocks(blockTasks);
|
||||
|
||||
// 3. Flatten the *sorted* task structure back to text lines
|
||||
// This text contains only the task lines, in their new sorted order,
|
||||
// maintaining original markdown (including indentation).
|
||||
const newSortedTasksOnlyText = flattenTasksToText(sortedBlockTasks);
|
||||
|
||||
// 4. Reconstruct the block text, preserving non-task lines
|
||||
const originalBlockLines = originalBlockText.split("\n");
|
||||
const sortedTaskLines = newSortedTasksOnlyText.split("\n"); // Individual sorted task lines
|
||||
|
||||
// Create a map from original document line number -> original task object
|
||||
// This helps identify which original lines were tasks.
|
||||
const originalTaskMap = new Map<number, SortableTask>();
|
||||
function populateOriginalTaskMap(tasks: SortableTask[]) {
|
||||
tasks.forEach((task) => {
|
||||
originalTaskMap.set(task.lineNumber, task);
|
||||
if (task.children) populateOriginalTaskMap(task.children);
|
||||
});
|
||||
// 2. Sort each continuous block separately
|
||||
for (let i = 0; i < taskBlocks.length; i++) {
|
||||
// Replace tasks in the original block with sorted tasks
|
||||
taskBlocks[i] = sortTasksRecursively(
|
||||
taskBlocks[i],
|
||||
sortCriteria,
|
||||
settings
|
||||
);
|
||||
}
|
||||
populateOriginalTaskMap(blockTasks); // Use tasks before sorting
|
||||
|
||||
// Create a map from original document line number -> its *new* sorted line content
|
||||
// This maps where each original task line should end up in the sorted output.
|
||||
const sortedLineContentMap = new Map<number, string>();
|
||||
let sortedLineIdx = 0;
|
||||
function buildSortedLineContentMap(sortedTasks: SortableTask[]) {
|
||||
sortedTasks.forEach((task) => {
|
||||
if (sortedLineIdx < sortedTaskLines.length) {
|
||||
// Map the task's original line number to the corresponding line
|
||||
// from the flattened sorted text.
|
||||
sortedLineContentMap.set(
|
||||
task.lineNumber,
|
||||
sortedTaskLines[sortedLineIdx++]
|
||||
);
|
||||
}
|
||||
if (task.children) buildSortedLineContentMap(task.children);
|
||||
});
|
||||
}
|
||||
buildSortedLineContentMap(sortedBlockTasks); // Use the sorted task structure
|
||||
// 3. Update the original blockTasks to reflect sorting results
|
||||
// Clear the original blockTasks
|
||||
blockTasks.length = 0;
|
||||
|
||||
let newBlockLines: string[] = [];
|
||||
for (let i = 0; i < originalBlockLines.length; i++) {
|
||||
const currentOriginalDocLineNumber = startLine + i;
|
||||
// Check if the line number in the original block corresponded to a task
|
||||
if (originalTaskMap.has(currentOriginalDocLineNumber)) {
|
||||
// It was a task. Get its new content from the sorted map.
|
||||
const newContent = sortedLineContentMap.get(
|
||||
currentOriginalDocLineNumber
|
||||
);
|
||||
if (newContent !== undefined) {
|
||||
newBlockLines.push(newContent);
|
||||
} else {
|
||||
// This case indicates a mismatch between parsed tasks and sorted lines, potentially
|
||||
// due to issues in flattening or mapping complex hierarchies. Fallback safely.
|
||||
console.warn(
|
||||
`Sort Tasks: Missing sorted content for task at original line ${currentOriginalDocLineNumber}. Keeping original line.`
|
||||
);
|
||||
newBlockLines.push(originalBlockLines[i]); // Keep original if lookup fails
|
||||
}
|
||||
} else {
|
||||
// It was not a task line, keep the original line
|
||||
newBlockLines.push(originalBlockLines[i]);
|
||||
// Merge all sorted blocks back into blockTasks
|
||||
for (const block of taskBlocks) {
|
||||
for (const task of block) {
|
||||
blockTasks.push(task);
|
||||
}
|
||||
}
|
||||
const newBlockText = newBlockLines.join("\n");
|
||||
|
||||
// 5. Calculate changes only if the block text has actually changed
|
||||
// 4. Rebuild text directly from sorted blockTasks
|
||||
const originalBlockLines = originalBlockText.split("\n");
|
||||
let newBlockLines: string[] = [...originalBlockLines]; // Copy original lines
|
||||
const processedLineIndices = new Set<number>(); // Track processed line indices
|
||||
|
||||
// Find indices of all task lines
|
||||
const taskLineIndices = new Set<number>();
|
||||
for (const task of blockTasks) {
|
||||
// Convert to index relative to block
|
||||
const relativeIndex = task.lineNumber - startLine;
|
||||
if (relativeIndex >= 0 && relativeIndex < originalBlockLines.length) {
|
||||
taskLineIndices.add(relativeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// For each task block, find its starting position and sort tasks at that position
|
||||
for (const block of taskBlocks) {
|
||||
// Find the minimum line number (relative to block)
|
||||
let minRelativeLineIndex = Number.MAX_SAFE_INTEGER;
|
||||
for (const task of block) {
|
||||
const relativeIndex = task.lineNumber - startLine;
|
||||
if (
|
||||
relativeIndex >= 0 &&
|
||||
relativeIndex < originalBlockLines.length &&
|
||||
relativeIndex < minRelativeLineIndex
|
||||
) {
|
||||
minRelativeLineIndex = relativeIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (minRelativeLineIndex === Number.MAX_SAFE_INTEGER) {
|
||||
continue; // Skip invalid blocks
|
||||
}
|
||||
|
||||
// Collect all task line content in this block
|
||||
const blockContent: string[] = [];
|
||||
|
||||
// Recursively add tasks and their subtasks
|
||||
function addSortedTaskContent(task: SortableTask) {
|
||||
blockContent.push(task.originalMarkdown);
|
||||
|
||||
// Mark this line as processed
|
||||
const relativeIndex = task.lineNumber - startLine;
|
||||
if (
|
||||
relativeIndex >= 0 &&
|
||||
relativeIndex < originalBlockLines.length
|
||||
) {
|
||||
processedLineIndices.add(relativeIndex);
|
||||
}
|
||||
|
||||
// Process subtasks
|
||||
if (task.children && task.children.length > 0) {
|
||||
for (const child of task.children) {
|
||||
addSortedTaskContent(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only process top-level tasks
|
||||
for (const task of block) {
|
||||
if (!task.parent) {
|
||||
// Only process top-level tasks
|
||||
addSortedTaskContent(task);
|
||||
}
|
||||
}
|
||||
|
||||
// Replace content at original position
|
||||
let currentLine = minRelativeLineIndex;
|
||||
for (const line of blockContent) {
|
||||
newBlockLines[currentLine++] = line;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove processed lines (replaced task lines)
|
||||
const finalLines: string[] = [];
|
||||
for (let i = 0; i < newBlockLines.length; i++) {
|
||||
if (!taskLineIndices.has(i) || processedLineIndices.has(i)) {
|
||||
finalLines.push(newBlockLines[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const newBlockText = finalLines.join("\n");
|
||||
|
||||
// 5. Only return new text if the block actually changed
|
||||
if (originalBlockText === newBlockText) {
|
||||
const noticeMsg = `Sort Tasks: Tasks are already sorted in the ${scopeMessage} (Lines ${
|
||||
startLine + 1
|
||||
}-${endLine + 1}).`;
|
||||
console.log(noticeMsg);
|
||||
new Notice(noticeMsg);
|
||||
return null;
|
||||
}
|
||||
|
||||
const changes: ChangeSpec = {
|
||||
from: fromOffsetOriginal,
|
||||
to: toOffsetOriginal,
|
||||
insert: newBlockText,
|
||||
};
|
||||
|
||||
const noticeMsg = `Sort Tasks: Sorted tasks in the ${scopeMessage} (Lines ${
|
||||
startLine + 1
|
||||
}-${endLine + 1}).`;
|
||||
console.log(noticeMsg);
|
||||
new Notice(noticeMsg);
|
||||
return [changes];
|
||||
|
||||
// Directly return the changed text
|
||||
return newBlockText;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Editor, MarkdownFileInfo, MarkdownView } from "obsidian";
|
||||
import TaskProgressBarPlugin from "..";
|
||||
import { taskStatusChangeAnnotation } from "../editor-ext/taskStatusSwitcher";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
|
||||
/**
|
||||
* Cycles the task status on the current line forward
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
MarkdownView,
|
||||
MarkdownFileInfo,
|
||||
} from "obsidian";
|
||||
import TaskProgressBarPlugin from "..";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import { buildIndentString } from "../utils";
|
||||
import { t } from "../translations/helper";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { AbstractInputSuggest, App, prepareFuzzySearch, TFile } from "obsidian";
|
||||
import TaskProgressBarPlugin from "src";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
|
||||
abstract class BaseSuggest<T> extends AbstractInputSuggest<T> {
|
||||
constructor(app: App, public inputEl: HTMLInputElement) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { debounce, getIconIds, Notice, setIcon } from "obsidian";
|
||||
|
||||
import { ButtonComponent } from "obsidian";
|
||||
import TaskProgressBarPlugin from "src";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
|
||||
export const attachIconMenu = (
|
||||
btn: ButtonComponent,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { App, Modal, Setting } from "obsidian";
|
||||
import { RewardItem } from "../common/setting-definition";
|
||||
import { t } from "src/translations/helper";
|
||||
import { t } from "../translations/helper";
|
||||
import "../styles/reward.css";
|
||||
|
||||
export class RewardModal extends Modal {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ExtraButtonComponent, Menu } from "obsidian";
|
||||
import { Component } from "obsidian";
|
||||
import TaskProgressBarPlugin from "src";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import { Task } from "../utils/types/TaskIndex";
|
||||
import { createTaskCheckbox } from "./task-view/details";
|
||||
import { getStatusText } from "./task-view/details";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Modal, App, Setting } from "obsidian";
|
||||
import TaskProgressBarPlugin from "src";
|
||||
import { WorkflowStage } from "src/common/setting-definition";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import { WorkflowStage } from "../common/setting-definition";
|
||||
import { t } from "../translations/helper";
|
||||
import { StageEditModal } from "./StageEditModal";
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
moment,
|
||||
setIcon,
|
||||
} from "obsidian";
|
||||
import { Task } from "src/utils/types/TaskIndex"; // Assuming Task type exists here
|
||||
import { Task } from "../../utils/types/TaskIndex"; // Assuming Task type exists here
|
||||
// Removed: import { renderCalendarEvent } from "./event";
|
||||
import "../../styles/calendar/view.css"; // Import the CSS file
|
||||
import "../../styles/calendar/event.css"; // Import the CSS file
|
||||
|
|
@ -19,7 +19,7 @@ import { WeekView } from "./views/week-view";
|
|||
import { DayView } from "./views/day-view";
|
||||
import { AgendaView } from "./views/agenda-view";
|
||||
import { YearView } from "./views/year-view";
|
||||
import TaskProgressBarPlugin from "src";
|
||||
import TaskProgressBarPlugin from "../../index";
|
||||
import { QuickCaptureModal } from "../QuickCaptureModal";
|
||||
// Import algorithm functions (optional for now, could be used within views)
|
||||
// import { calculateEventLayout, determineEventColor } from './algorithm';
|
||||
|
|
|
|||
|
|
@ -1,16 +1,10 @@
|
|||
import {
|
||||
App,
|
||||
Component,
|
||||
debounce,
|
||||
moment,
|
||||
MarkdownRenderer as ObsidianMarkdownRenderer,
|
||||
} from "obsidian";
|
||||
import { CalendarEvent } from ".."; // Adjust path as needed
|
||||
import { App, Component, debounce, moment } from "obsidian";
|
||||
import { CalendarEvent } from "../index"; // Adjust path as needed
|
||||
import { EventLayout, determineEventColor } from "../algorithm"; // Adjust path as needed
|
||||
import {
|
||||
clearAllMarks,
|
||||
MarkdownRendererComponent,
|
||||
} from "src/components/MarkdownRenderer";
|
||||
} from "../../MarkdownRenderer";
|
||||
import { createTaskCheckbox } from "../../task-view/details";
|
||||
|
||||
export type EventViewType =
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { App, Component, moment } from "obsidian";
|
||||
import { CalendarEvent } from "..";
|
||||
import { CalendarEvent } from "../index";
|
||||
import { renderCalendarEvent } from "../rendering/event-renderer"; // Use new renderer
|
||||
import { CalendarViewComponent, CalendarViewOptions } from "./base-view"; // Import base class
|
||||
import TaskProgressBarPlugin from "../../../index"; // Import plugin type
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { App, Component } from "obsidian";
|
||||
import { CalendarEvent } from "..";
|
||||
import TaskProgressBarPlugin from "src";
|
||||
import { CalendarEvent } from "../index";
|
||||
import TaskProgressBarPlugin from "../../../index";
|
||||
|
||||
interface EventMap {
|
||||
onEventClick: (ev: MouseEvent, event: CalendarEvent) => void;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { App, Component, debounce, moment } from "obsidian";
|
||||
import { CalendarEvent } from "..";
|
||||
import {
|
||||
renderCalendarEvent,
|
||||
EventViewType,
|
||||
} from "../rendering/event-renderer";
|
||||
import { App, Component, moment } from "obsidian";
|
||||
import { CalendarEvent } from "../index";
|
||||
import { renderCalendarEvent } from "../rendering/event-renderer";
|
||||
import { CalendarViewComponent, CalendarViewOptions } from "./base-view";
|
||||
import TaskProgressBarPlugin from "../../../index";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
import { App, Component, debounce, moment } from "obsidian";
|
||||
import { CalendarEvent } from "..";
|
||||
import {
|
||||
calculateEventLayout,
|
||||
determineEventColor,
|
||||
EventLayout,
|
||||
} from "../algorithm"; // Import layout functions
|
||||
import { CalendarEvent } from "../index";
|
||||
import { renderCalendarEvent } from "../rendering/event-renderer"; // Use new renderer
|
||||
import {
|
||||
CalendarSpecificConfig,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { App, Component, debounce, moment } from "obsidian";
|
||||
import { CalendarEvent } from "..";
|
||||
import { CalendarEvent } from "../index";
|
||||
import {
|
||||
CalendarSpecificConfig,
|
||||
getViewSettingOrDefault,
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ import { DateHelper } from "../../utils/DateHelper";
|
|||
import { TimelineHeaderComponent } from "./timeline-header";
|
||||
import { GridBackgroundComponent } from "./grid-background";
|
||||
import { TaskRendererComponent } from "./task-renderer";
|
||||
import TaskProgressBarPlugin from "src";
|
||||
import TaskProgressBarPlugin from "../../index";
|
||||
import { FilterComponent, buildFilterOptionsFromTasks } from "../filter/filter";
|
||||
import { ActiveFilter, FilterCategory } from "../filter/filter-type";
|
||||
import { ScrollToDateButton } from "../filter/custom/scroll-to-date-button";
|
||||
import { PRIORITY_MAP } from "src/common/default-symbol";
|
||||
import { PRIORITY_MAP } from "../../common/default-symbol";
|
||||
|
||||
// Define the PRIORITY_MAP here as well, or import it if moved to a shared location
|
||||
// This is needed to convert filter value (icon/text) back to number for comparison
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ import {
|
|||
ScheduledHabitProps,
|
||||
MappingHabitProps,
|
||||
} from "../../types/habit-card"; // Assuming types are in src/types
|
||||
import TaskProgressBarPlugin from "src";
|
||||
import TaskProgressBarPlugin from "../../index";
|
||||
import {
|
||||
DailyHabitCard,
|
||||
CountHabitCard,
|
||||
ScheduledHabitCard,
|
||||
MappingHabitCard,
|
||||
} from "./habitcard/index"; // Import the habit card classes
|
||||
import { t } from "src/translations/helper";
|
||||
import { t } from "../../translations/helper";
|
||||
import "../../styles/habit.css";
|
||||
|
||||
export class Habit extends Component {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { ButtonComponent, Component, Notice, setIcon } from "obsidian";
|
||||
import { CountHabitProps } from "src/types/habit-card";
|
||||
import { CountHabitProps } from "../../../types/habit-card";
|
||||
import { HabitCard } from "./habitcard";
|
||||
import { t } from "src/translations/helper";
|
||||
import TaskProgressBarPlugin from "src/index";
|
||||
import { t } from "../../../translations/helper";
|
||||
import TaskProgressBarPlugin from "../../../index";
|
||||
|
||||
export class CountHabitCard extends HabitCard {
|
||||
constructor(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Component, Notice, setIcon } from "obsidian";
|
||||
import { DailyHabitProps } from "src/types/habit-card";
|
||||
import { DailyHabitProps } from "../../../types/habit-card";
|
||||
import { HabitCard } from "./habitcard";
|
||||
import { t } from "src/translations/helper";
|
||||
import TaskProgressBarPlugin from "src/index";
|
||||
import { t } from "../../../translations/helper";
|
||||
import TaskProgressBarPlugin from "../../../index";
|
||||
|
||||
export class DailyHabitCard extends HabitCard {
|
||||
constructor(
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import {
|
|||
DailyHabitProps,
|
||||
HabitProps,
|
||||
MappingHabitProps,
|
||||
} from "src/types/habit-card";
|
||||
import TaskProgressBarPlugin from "src/index";
|
||||
} from "../../../types/habit-card";
|
||||
import TaskProgressBarPlugin from "../../../index";
|
||||
|
||||
function getDatesInRange(startDate: string, endDate: string): string[] {
|
||||
const dates = [];
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import {
|
|||
Setting,
|
||||
SliderComponent,
|
||||
} from "obsidian";
|
||||
import { MappingHabitProps } from "src/types/habit-card";
|
||||
import { MappingHabitProps } from "../../../types/habit-card";
|
||||
import { HabitCard } from "./habitcard";
|
||||
import TaskProgressBarPlugin from "src";
|
||||
import TaskProgressBarPlugin from "../../../index";
|
||||
|
||||
export class MappingHabitCard extends HabitCard {
|
||||
constructor(
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import {
|
|||
setIcon,
|
||||
Setting,
|
||||
} from "obsidian";
|
||||
import { ScheduledHabitProps } from "src/types/habit-card";
|
||||
import { ScheduledHabitProps } from "../../../types/habit-card";
|
||||
import { HabitCard } from "./habitcard";
|
||||
import TaskProgressBarPlugin from "src";
|
||||
import { t } from "src/translations/helper";
|
||||
import TaskProgressBarPlugin from "../../../index";
|
||||
import { t } from "../../../translations/helper";
|
||||
import { EventDetailModal } from "../habit";
|
||||
|
||||
function renderPieDotSVG(completed: number, total: number): string {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { App, Component, MarkdownRenderer, Menu, TFile } from "obsidian";
|
||||
import { Task } from "src/utils/types/TaskIndex"; // Adjust path
|
||||
import { Task } from "../../utils/types/TaskIndex"; // Adjust path
|
||||
import { MarkdownRendererComponent } from "../MarkdownRenderer"; // Adjust path
|
||||
import TaskProgressBarPlugin from "../../index"; // Adjust path
|
||||
import { KanbanSpecificConfig } from "../../common/setting-definition";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { App, Component, setIcon } from "obsidian";
|
||||
import { Task } from "src/utils/types/TaskIndex"; // Adjust path
|
||||
import { Task } from "../../utils/types/TaskIndex"; // Adjust path
|
||||
import { KanbanCardComponent } from "./kanban-card";
|
||||
import TaskProgressBarPlugin from "../../index"; // Adjust path
|
||||
import { QuickCaptureModal } from "../QuickCaptureModal"; // Import QuickCaptureModal
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { App, Component, WorkspaceLeaf } from "obsidian";
|
||||
import TaskProgressBarPlugin from "../../index"; // Adjust path as needed
|
||||
import { Task } from "src/utils/types/TaskIndex"; // Adjust path as needed
|
||||
import { Task } from "../../utils/types/TaskIndex"; // Adjust path as needed
|
||||
import { KanbanColumnComponent } from "./kanban-column";
|
||||
import { DragManager, DragMoveEvent, DragEndEvent } from "../DragManager";
|
||||
import "../../styles/kanban/kanban.css";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import TaskProgressBarPlugin from "..";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import {
|
||||
Component,
|
||||
debounce,
|
||||
|
|
@ -12,7 +12,7 @@ import {
|
|||
checkIfParentElementHasGoalFormat,
|
||||
extractTaskAndGoalInfoReadMode,
|
||||
getCustomTotalGoalReadMode,
|
||||
} from "src/utils/goal/readMode";
|
||||
} from "../utils/goal/readMode";
|
||||
|
||||
interface GroupElement {
|
||||
parentElement: HTMLElement;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
TFile,
|
||||
} from "obsidian";
|
||||
import { getTasksAPI } from "../utils";
|
||||
import { parseTaskLine } from "src/utils/taskUtil";
|
||||
import { parseTaskLine } from "../utils/taskUtil";
|
||||
|
||||
// This component replaces standard checkboxes with custom text marks in reading view
|
||||
export function applyTaskTextMarks({
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { Task } from "../../utils/types/TaskIndex";
|
|||
import { t } from "../../translations/helper";
|
||||
import "../../styles/project-view.css";
|
||||
import { TaskListRendererComponent } from "./TaskList";
|
||||
import TaskProgressBarPlugin from "src";
|
||||
import TaskProgressBarPlugin from "../../index";
|
||||
|
||||
interface SelectedProjects {
|
||||
projects: string[];
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
ViewMode,
|
||||
getViewSettingOrDefault,
|
||||
} from "../../common/setting-definition";
|
||||
import { TASK_SPECIFIC_VIEW_TYPE } from "src/pages/TaskSpecificView";
|
||||
import { TASK_SPECIFIC_VIEW_TYPE } from "../../pages/TaskSpecificView";
|
||||
import { ViewConfigModal } from "../ViewConfigModal";
|
||||
|
||||
// Remove the enum if it exists, use ViewMode type directly
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from "@codemirror/state";
|
||||
import { getTabSize } from "../utils";
|
||||
import { taskStatusChangeAnnotation } from "./taskStatusSwitcher";
|
||||
import TaskProgressBarPlugin from "..";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import {
|
||||
isLastWorkflowStageOrNotWorkflow,
|
||||
workflowChangeAnnotation,
|
||||
|
|
@ -126,12 +126,6 @@ function handleParentTaskUpdateTransaction(
|
|||
) {
|
||||
const inProgressStatuses =
|
||||
plugin.settings.taskStatuses.inProgress.split("|") || ["/"];
|
||||
// Prevent updating parent if the trigger was an auto-complete itself
|
||||
console.log(
|
||||
isAutoCompleteAnnotation,
|
||||
anySiblingsWithStatus,
|
||||
tr.changes
|
||||
);
|
||||
if (!isAutoCompleteAnnotation) {
|
||||
return markParentAsInProgress(
|
||||
tr,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import {
|
|||
Transaction,
|
||||
TransactionSpec,
|
||||
} from "@codemirror/state";
|
||||
import TaskProgressBarPlugin from "..";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import { taskStatusChangeAnnotation } from "./taskStatusSwitcher";
|
||||
import { getTasksAPI } from "../utils";
|
||||
import { priorityChangeAnnotation } from "./priorityPicker";
|
||||
import { parseTaskLine } from "src/utils/taskUtil";
|
||||
import { parseTaskLine } from "../utils/taskUtil";
|
||||
/**
|
||||
* Creates an editor extension that cycles through task statuses when a user clicks on a task marker
|
||||
* @param app The Obsidian app instance
|
||||
|
|
@ -279,8 +279,6 @@ export function findTaskStatusChanges(
|
|||
isTaskChange = true;
|
||||
}
|
||||
|
||||
console.log(newLineText, insertedText);
|
||||
|
||||
if (
|
||||
tasksPluginLoaded &&
|
||||
newLineText === insertedText &&
|
||||
|
|
@ -296,14 +294,6 @@ export function findTaskStatusChanges(
|
|||
triggerByTasks = true;
|
||||
}
|
||||
|
||||
console.log({
|
||||
triggerByTasks,
|
||||
wasCompleteTask,
|
||||
isTaskChange,
|
||||
changedPosition,
|
||||
currentMark,
|
||||
});
|
||||
|
||||
if (
|
||||
changedPosition !== null &&
|
||||
currentMark !== null &&
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
PluginSpec,
|
||||
} from "@codemirror/view";
|
||||
import { App, editorLivePreviewField, Menu, MenuItem, moment } from "obsidian";
|
||||
import TaskProgressBarPlugin from "..";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import { Annotation } from "@codemirror/state";
|
||||
// @ts-ignore - This import is necessary but TypeScript can't find it
|
||||
import { syntaxTree, tokenClassNodeProp } from "@codemirror/language";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { App, editorInfoField } from "obsidian";
|
||||
import { EditorState, Transaction, Text } from "@codemirror/state";
|
||||
import TaskProgressBarPlugin from ".."; // Adjust path if needed
|
||||
import TaskProgressBarPlugin from "../index"; // Adjust path if needed
|
||||
import { parseTaskLine } from "../utils/taskUtil"; // Adjust path if needed
|
||||
|
||||
/**
|
||||
|
|
@ -33,7 +33,6 @@ function handleMonitorTaskCompletionTransaction(
|
|||
app: App,
|
||||
plugin: TaskProgressBarPlugin
|
||||
) {
|
||||
console.log("handleMonitorTaskCompletionTransaction");
|
||||
// Only process transactions that change the document
|
||||
if (!tr.docChanged) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
PluginSpec,
|
||||
} from "@codemirror/view";
|
||||
import { App, editorLivePreviewField, Keymap, Menu } from "obsidian";
|
||||
import TaskProgressBarPlugin from "..";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import { Annotation } from "@codemirror/state";
|
||||
// @ts-ignore - This import is necessary but TypeScript can't find it
|
||||
import { syntaxTree, tokenClassNodeProp } from "@codemirror/language";
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import { EditorState, Range, Text } from "@codemirror/state";
|
|||
// @ts-ignore - This import is necessary but TypeScript can't find it
|
||||
import { foldable, syntaxTree, tokenClassNodeProp } from "@codemirror/language";
|
||||
import { RegExpCursor } from "./regexp-cursor";
|
||||
import TaskProgressBarPlugin, { showPopoverWithProgressBar } from "..";
|
||||
import TaskProgressBarPlugin, { showPopoverWithProgressBar } from "../index";
|
||||
import { shouldHideProgressBarInLivePriview } from "../utils";
|
||||
import "../styles/progressbar.css";
|
||||
import { extractTaskAndGoalInfo } from "src/utils/goal/editMode";
|
||||
import { extractTaskAndGoalInfo } from "../utils/goal/editMode";
|
||||
|
||||
interface Tasks {
|
||||
completed: number;
|
||||
|
|
|
|||
|
|
@ -16,12 +16,11 @@ import {
|
|||
Keymap,
|
||||
Menu,
|
||||
} from "obsidian";
|
||||
import TaskProgressBarPlugin from "..";
|
||||
import { Annotation, EditorSelection, SelectionRange } from "@codemirror/state";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import { Annotation, EditorSelection } from "@codemirror/state";
|
||||
// @ts-ignore - This import is necessary but TypeScript can't find it
|
||||
import { foldable, syntaxTree, tokenClassNodeProp } from "@codemirror/language";
|
||||
import { getTasksAPI } from "src/utils";
|
||||
import { parseTaskLine } from "src/utils/taskUtil";
|
||||
import { syntaxTree, tokenClassNodeProp } from "@codemirror/language";
|
||||
import { getTasksAPI } from "../utils";
|
||||
|
||||
export type TaskState = string;
|
||||
export const taskStatusChangeAnnotation = Annotation.define();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
Text,
|
||||
} from "@codemirror/state";
|
||||
import { Annotation } from "@codemirror/state";
|
||||
import TaskProgressBarPlugin from "..";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import { taskStatusChangeAnnotation } from "./taskStatusSwitcher";
|
||||
import { priorityChangeAnnotation } from "./priorityPicker";
|
||||
import { buildIndentString, getTabSize } from "../utils";
|
||||
|
|
|
|||
|
|
@ -466,7 +466,11 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
const changes = sortTasksInDocument(editorView, this, criteria);
|
||||
|
||||
if (changes) {
|
||||
editorView.dispatch({ changes });
|
||||
const info = editorView.state.field(editorInfoField);
|
||||
if (!info || !info.file) return;
|
||||
this.app.vault.process(info.file, (data) => {
|
||||
return changes;
|
||||
});
|
||||
} else {
|
||||
new Notice("Tasks already sorted or no tasks found.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ import { CalendarComponent, CalendarEvent } from "../components/calendar";
|
|||
import { KanbanComponent } from "../components/kanban/kanban";
|
||||
import { GanttComponent } from "../components/gantt/gantt";
|
||||
import { TaskPropertyTwoColumnView } from "../components/task-view/TaskPropertyTwoColumnView";
|
||||
import { Habit } from "src/components/habit/habit";
|
||||
import { Habit } from "../components/habit/habit";
|
||||
|
||||
export const TASK_VIEW_TYPE = "task-genius-view";
|
||||
|
||||
|
|
|
|||
|
|
@ -121,8 +121,6 @@ class TranslationManager {
|
|||
}
|
||||
}
|
||||
|
||||
console.log(this.currentLocale);
|
||||
|
||||
// If still not found, use fallback
|
||||
if (!result) {
|
||||
console.warn(
|
||||
|
|
@ -156,8 +154,6 @@ class TranslationManager {
|
|||
// Remove leading/trailing quotes if present
|
||||
result = result.replace(/^["""']|["""']$/g, "");
|
||||
|
||||
console.log(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
import { TaskIndexer } from "./import/TaskIndexer";
|
||||
import { TaskWorkerManager } from "./workers/TaskWorkerManager";
|
||||
import { LocalStorageCache } from "./persister";
|
||||
import TaskProgressBarPlugin from "src";
|
||||
import TaskProgressBarPlugin from "../index";
|
||||
import { RRule, RRuleSet, rrulestr } from "rrule";
|
||||
|
||||
/**
|
||||
|
|
@ -1515,37 +1515,70 @@ export class TaskManager extends Component {
|
|||
|
||||
// Start with current due date or today if no due date
|
||||
const baseDate = task.dueDate ? new Date(task.dueDate) : new Date();
|
||||
// Ensure baseDate is at the beginning of the day for date-based recurrence
|
||||
baseDate.setHours(0, 0, 0, 0);
|
||||
|
||||
try {
|
||||
// Parse different recurrence formats
|
||||
// Tasks plugin style: "every day", "every 2 weeks", etc.
|
||||
const recurrence = task.recurrence.trim().toLowerCase();
|
||||
// Attempt to parse using rrule first
|
||||
try {
|
||||
// Use the task's recurrence string directly if it's a valid RRULE string
|
||||
// Provide dtstart to rrulestr for context, especially for rules that might depend on the start date.
|
||||
const rule = rrulestr(task.recurrence, { dtstart: baseDate });
|
||||
|
||||
// Default to adding 1 day if we can't parse the recurrence
|
||||
let nextDate = new Date(baseDate);
|
||||
nextDate.setDate(nextDate.getDate() + 1);
|
||||
// We want the first occurrence strictly *after* the baseDate.
|
||||
// Adding a small time offset ensures we get the next instance even if baseDate itself is an occurrence.
|
||||
const afterDate = new Date(baseDate.getTime() + 1000); // 1 second after baseDate
|
||||
const nextOccurrence = rule.after(afterDate); // Find the first occurrence after this adjusted date
|
||||
|
||||
if (nextOccurrence) {
|
||||
// Set time to start of day, assuming date-only recurrence for now
|
||||
nextOccurrence.setHours(0, 0, 0, 0);
|
||||
this.log(
|
||||
`Calculated next date using rrule for '${
|
||||
task.recurrence
|
||||
}': ${nextOccurrence.toISOString()}`
|
||||
);
|
||||
return nextOccurrence.getTime();
|
||||
} else {
|
||||
// No next occurrence found by rrule (e.g., rule has COUNT and finished)
|
||||
this.log(
|
||||
`[TaskManager] rrule couldn't find next occurrence for rule: ${task.recurrence}. Falling back.`
|
||||
);
|
||||
// Fall through to simple logic below
|
||||
}
|
||||
} catch (e) {
|
||||
// rrulestr failed, likely not a standard RRULE format. Fall back to simple parsing.
|
||||
if (e instanceof Error) {
|
||||
this.log(
|
||||
`[TaskManager] Failed to parse recurrence '${task.recurrence}' with rrule. Falling back to simple logic. Error: ${e.message}`
|
||||
);
|
||||
} else {
|
||||
this.log(
|
||||
`[TaskManager] Failed to parse recurrence '${task.recurrence}' with rrule. Falling back to simple logic. Unknown error.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fallback Simple Parsing Logic ---
|
||||
this.log(
|
||||
`[TaskManager] Using fallback logic for recurrence: ${task.recurrence}`
|
||||
);
|
||||
const recurrence = task.recurrence.trim().toLowerCase();
|
||||
let nextDate = new Date(baseDate); // Start calculation from the base date
|
||||
|
||||
// Parse "every X days/weeks/months/years" format
|
||||
if (recurrence.startsWith("every")) {
|
||||
const parts = recurrence.split(" ");
|
||||
|
||||
// Handle "every day/week/month/year"
|
||||
if (parts.length >= 2) {
|
||||
let interval = 1;
|
||||
let unit = parts[1];
|
||||
|
||||
// Check if there's a number after "every"
|
||||
if (parts.length >= 3 && !isNaN(parseInt(parts[1]))) {
|
||||
interval = parseInt(parts[1]);
|
||||
unit = parts[2];
|
||||
}
|
||||
|
||||
// Make unit singular for matching
|
||||
if (unit.endsWith("s")) {
|
||||
unit = unit.substring(0, unit.length - 1);
|
||||
}
|
||||
|
||||
// Calculate next date based on unit
|
||||
switch (unit) {
|
||||
case "day":
|
||||
nextDate.setDate(baseDate.getDate() + interval);
|
||||
|
|
@ -1562,9 +1595,17 @@ export class TaskManager extends Component {
|
|||
);
|
||||
break;
|
||||
default:
|
||||
// For unknown units, default to days
|
||||
nextDate.setDate(baseDate.getDate() + interval);
|
||||
this.log(
|
||||
`[TaskManager] Unknown unit in recurrence '${recurrence}'. Defaulting to days.`
|
||||
);
|
||||
nextDate.setDate(baseDate.getDate() + interval); // Default to days
|
||||
}
|
||||
} else {
|
||||
// Malformed "every" rule, fallback to +1 day from baseDate
|
||||
this.log(
|
||||
`[TaskManager] Malformed 'every' rule '${recurrence}'. Defaulting to next day.`
|
||||
);
|
||||
nextDate.setDate(baseDate.getDate() + 1);
|
||||
}
|
||||
}
|
||||
// Handle specific weekday recurrences like "every Monday"
|
||||
|
|
@ -1577,7 +1618,7 @@ export class TaskManager extends Component {
|
|||
recurrence.includes("saturday") ||
|
||||
recurrence.includes("sunday")
|
||||
) {
|
||||
const weekdays = {
|
||||
const weekdays: { [key: string]: number } = {
|
||||
sunday: 0,
|
||||
monday: 1,
|
||||
tuesday: 2,
|
||||
|
|
@ -1586,8 +1627,6 @@ export class TaskManager extends Component {
|
|||
friday: 5,
|
||||
saturday: 6,
|
||||
};
|
||||
|
||||
// Find which weekday is mentioned
|
||||
let targetDay = -1;
|
||||
for (const [day, value] of Object.entries(weekdays)) {
|
||||
if (recurrence.includes(day)) {
|
||||
|
|
@ -1595,25 +1634,53 @@ export class TaskManager extends Component {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetDay >= 0) {
|
||||
// Start from tomorrow to avoid repeating on the same day
|
||||
// Start calculation from the day *after* the baseDate
|
||||
nextDate.setDate(baseDate.getDate() + 1);
|
||||
|
||||
// Find the next occurrence of the target day
|
||||
while (nextDate.getDay() !== targetDay) {
|
||||
nextDate.setDate(nextDate.getDate() + 1);
|
||||
}
|
||||
} else {
|
||||
// Malformed weekday rule, fallback to +1 day from baseDate
|
||||
this.log(
|
||||
`[TaskManager] Malformed weekday rule '${recurrence}'. Defaulting to next day.`
|
||||
);
|
||||
nextDate.setDate(baseDate.getDate() + 1);
|
||||
}
|
||||
} else {
|
||||
// Unknown format, fallback to +1 day from baseDate
|
||||
this.log(
|
||||
`[TaskManager] Unknown recurrence format '${recurrence}'. Defaulting to next day.`
|
||||
);
|
||||
nextDate.setDate(baseDate.getDate() + 1);
|
||||
}
|
||||
|
||||
// Ensure the calculated date is at the start of the day
|
||||
nextDate.setHours(0, 0, 0, 0);
|
||||
this.log(
|
||||
`Calculated next date using simple logic for '${
|
||||
task.recurrence
|
||||
}': ${nextDate.toISOString()}`
|
||||
);
|
||||
return nextDate.getTime();
|
||||
} catch (error) {
|
||||
console.error("Error calculating next due date:", error);
|
||||
// Default fallback: add one day
|
||||
const tomorrow = new Date(baseDate);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
return tomorrow.getTime();
|
||||
// Default fallback: add one day to baseDate
|
||||
const fallbackDate = new Date(baseDate);
|
||||
fallbackDate.setDate(fallbackDate.getDate() + 1);
|
||||
fallbackDate.setHours(0, 0, 0, 0);
|
||||
if (task.recurrence) {
|
||||
this.log(
|
||||
`Error calculating next due date for '${
|
||||
task.recurrence
|
||||
}'. Defaulting to ${fallbackDate.toISOString()}`
|
||||
);
|
||||
} else {
|
||||
this.log(
|
||||
`Error calculating next due date for task without recurrence. Defaulting to ${fallbackDate.toISOString()}`
|
||||
);
|
||||
}
|
||||
return fallbackDate.getTime();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { PRIORITY_MAP } from "src/common/default-symbol";
|
||||
import { PRIORITY_MAP } from "../common/default-symbol";
|
||||
import { parseLocalDate } from "./dateUtil";
|
||||
import { Task } from "./types/TaskIndex";
|
||||
import {
|
||||
|
|
|
|||
Loading…
Reference in a new issue