Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: find tasks in blockquotes and Obsidian callouts #953

Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
577570f
fix: find tasks in blockquotes and Obsidian callouts
AnnaKornfeldSimpson Jul 30, 2022
d16c07d
fix: Update comment about changed regex
AnnaKornfeldSimpson Jul 30, 2022
1a5b63d
fix: CreateOrEdit should treat blockquotes or callouts the same as fr…
AnnaKornfeldSimpson Jul 30, 2022
c4d5309
test: Add indentation to initial blockquote parsing test
AnnaKornfeldSimpson Jul 30, 2022
f86db80
fix: More regexes needed updating.
AnnaKornfeldSimpson Jul 30, 2022
011d4ca
fix: attempt at logical definition of subitems inside blockquote or c…
AnnaKornfeldSimpson Jul 30, 2022
6df3ef0
Temporarily change the last test in Query.test.ts to put input in nes…
AnnaKornfeldSimpson Aug 3, 2022
a3f41de
fix: typo, since I am reading. No functionality change
AnnaKornfeldSimpson Aug 3, 2022
faa6893
Undo change to Query.test.ts
AnnaKornfeldSimpson Aug 4, 2022
77e0c55
fix: Properly detect sections of tasks inside callouts or blockquotes…
AnnaKornfeldSimpson Aug 4, 2022
e28d9ce
fix: Add warning to LivePreview that clicking checkbox inside callout…
AnnaKornfeldSimpson Aug 4, 2022
6c6e7ab
docs: Document this feature and LP caveat
AnnaKornfeldSimpson Aug 5, 2022
5a69989
test: Update Smoke Test to test tasks in callouts and blockquotes.
AnnaKornfeldSimpson Aug 5, 2022
12b6a73
fix: Better explanation in comments about LP issues.
AnnaKornfeldSimpson Aug 5, 2022
4565935
Add some tests with * as listmarker instead of -
AnnaKornfeldSimpson Aug 5, 2022
78f3005
fix: Lengthen Notice in LP for callouts from 3s to 30s
AnnaKornfeldSimpson Aug 5, 2022
b93d921
fix: Wording in Smoke Test file
AnnaKornfeldSimpson Aug 5, 2022
60f783e
fix: remove change to cache that is unrelated to callouts
AnnaKornfeldSimpson Aug 6, 2022
61585c4
fix: Clearer warning notice in LP about callouts
AnnaKornfeldSimpson Aug 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/getting-started/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ The following _does not work:_

---

Warning
{: .label .label-yellow}
Tasks can read tasks that are inside blockquotes or [Obsidian's built-in callouts](https://help.obsidian.md/How+to/Use+callouts).
However, in the Live Preview editor mode (pencil icon in lower right corner), tasks written inside callouts cannot be completed by clicking the checkbox.
You will see a warning; use the command `Tasks: Toggle Done`, or switch to Reading View (book icon in lower right corner) to click the checkbox.
Completing a task by clicking its checkbox from a `tasks` query block _will_ work in any editor mode, even if the query is inside a callout.

---

Warning
{: .label .label-yellow}

Tasks cannot read tasks that are inside code blocks, such as the ones used by the Admonitions plugin. Use Obsidian's built-in callouts instead.

---

Warning
{: .label .label-yellow}
Tasks can only render inline footnotes. Regular footnotes are not supported.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,17 @@ Work through all the tasks below, until zero tasks remain in this query:

### Recurring Tasks

Confirm that when a recurring task is completed, a new task is created and all the date fields are incremented.
Confirm that when a recurring task is completed, a new task is created, all the date fields are incremented, and the indentation is correct.
AnnaKornfeldSimpson marked this conversation as resolved.
Show resolved Hide resolved
AnnaKornfeldSimpson marked this conversation as resolved.
Show resolved Hide resolved

> [!Todo]
>
> - [ ] #task Complete this recurring task in **Source view** using **Tasks: Toggle task done** command 🔁 every day 🛫 2022-02-17 ⏳ 2022-02-18 📅 2022-02-19
>
> > - [ ] #task Complete this recurring task in **Reading view**🔁 every day 🛫 2022-02-17 ⏳ 2022-02-18 📅 2022-02-19

- [ ] #task Complete this recurring task in **Source view** using **Tasks: Toggle task done** command 🔁 every day 🛫 2022-02-17 ⏳ 2022-02-18 📅 2022-02-19
- [ ] #task Complete this recurring task in **Reading view**🔁 every day 🛫 2022-02-17 ⏳ 2022-02-18 📅 2022-02-19
- [ ] #task Complete this recurring task in **Live Preview**🔁 every day 🛫 2022-02-17 ⏳ 2022-02-18 📅 2022-02-19
- [ ] #task **check**: Checked all above steps for **recurring tasks** worked

> - [ ] #task **check**: Checked all above steps for **recurring tasks** worked

### Rendering of Task Blocks

Expand Down
60 changes: 18 additions & 42 deletions src/Cache.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MetadataCache, TAbstractFile, TFile, Vault } from 'obsidian';
import type { CachedMetadata, ListItemCache } from 'obsidian';
import type { EventRef, SectionCache } from 'obsidian';
import type { CachedMetadata, EventRef } from 'obsidian';
import type { HeadingCache, ListItemCache, SectionCache } from 'obsidian';
import { Mutex } from 'async-mutex';

import { Task } from './Task';
Expand Down Expand Up @@ -290,11 +290,10 @@ export class Cache {
path: file.path,
sectionStart: currentSection.position.start.line,
sectionIndex,
precedingHeader: Cache.getPrecedingHeader({
lineNumberTask: listItem.position.start.line,
sections: fileCache.sections,
fileLines,
}),
precedingHeader: Cache.getPrecedingHeader(
listItem.position.start.line,
fileCache.headings,
),
});

if (task !== null) {
Expand All @@ -320,7 +319,6 @@ export class Cache {

for (const section of sections) {
if (
section.type === 'list' &&
section.position.start.line <= lineNumberTask &&
claremacrae marked this conversation as resolved.
Show resolved Hide resolved
section.position.end.line >= lineNumberTask
) {
Expand All @@ -331,44 +329,22 @@ export class Cache {
return null;
}

private static getPrecedingHeader({
lineNumberTask,
sections,
fileLines,
}: {
lineNumberTask: number;
sections: SectionCache[] | undefined;
fileLines: string[];
}): string | null {
if (sections === undefined) {
return null;
}

let precedingHeaderSection: SectionCache | undefined;
for (const section of sections) {
if (section.type === 'heading') {
if (section.position.start.line > lineNumberTask) {
// Break out of the loop as the last header was the preceding one.
break;
}
precedingHeaderSection = section;
}
}
if (precedingHeaderSection === undefined) {
private static getPrecedingHeader(
lineNumberTask: number,
headings: HeadingCache[] | undefined,
): string | null {
if (headings === undefined) {
return null;
}

const lineNumberPrecedingHeader =
precedingHeaderSection.position.start.line;

const linePrecedingHeader = fileLines[lineNumberPrecedingHeader];
let precedingHeading: string | null = null;
AnnaKornfeldSimpson marked this conversation as resolved.
Show resolved Hide resolved

const headerRegex = /^#+ +(.*)/u;
const headerMatch = linePrecedingHeader.match(headerRegex);
if (headerMatch === null) {
return null;
} else {
return headerMatch[1];
for (const heading of headings) {
if (heading.position.start.line > lineNumberTask) {
return precedingHeading;
}
precedingHeading = heading.heading;
}
return precedingHeading;
}
}
2 changes: 1 addition & 1 deletion src/Commands/CreateOrEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const taskFromLine = ({ line, path }: { line: string; path: string }): Task => {

// If we are not on a line of a task, we take what we have.
// The non-task line can still be a checklist, for example if it is lacking the global filter.
const nonTaskRegex: RegExp = /^([\s\t]*)[-*]? *(\[(.)\])? *(.*)/u;
const nonTaskRegex: RegExp = /^([\s\t>]*)[-*]? *(\[(.)\])? *(.*)/u;
AnnaKornfeldSimpson marked this conversation as resolved.
Show resolved Hide resolved
const nonTaskMatch = line.match(nonTaskRegex);
if (nonTaskMatch === null) {
// Should never happen; everything in the regex is optional.
Expand Down
4 changes: 2 additions & 2 deletions src/Commands/ToggleDone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ const toggleLine = ({ line, path }: { line: string; path: string }): string => {
// 1. a list item
// 2. a simple text line

const listItemRegex = /^([\s\t]*)([-*])/;
const listItemRegex = /^([\s\t>]*)([-*])/;
if (listItemRegex.test(line)) {
// Let's convert the list item to a checklist item.
toggledLine = line.replace(listItemRegex, '$1$2 [ ]');
} else {
// Let's convert the line to a list item.
toggledLine = line.replace(/^([\s\t]*)/, '$1- ');
toggledLine = line.replace(/^([\s\t>]*)/, '$1- ');
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/Events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ interface CacheUpdateData {
export class Events {
private obsidianEvents: ObsidianEvents;

constructor({ obsidianEents }: { obsidianEents: ObsidianEvents }) {
this.obsidianEvents = obsidianEents;
constructor({ obsidianEvents }: { obsidianEvents: ObsidianEvents }) {
this.obsidianEvents = obsidianEvents;
}

public onCacheUpdate(
Expand Down
32 changes: 30 additions & 2 deletions src/LivePreviewExtension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EditorView, ViewPlugin } from '@codemirror/view';
import type { PluginValue } from '@codemirror/view';
import { Notice } from 'obsidian';

import { Task } from './Task';

Expand Down Expand Up @@ -33,8 +34,31 @@ class LivePreviewExtension implements PluginValue {
return false;
}

/* Right now Obsidian API does not give us a way to handle checkbox clicks inside rendered-widgets-in-LP such as
* callouts, tables, and transclusions because `this.view.posAtDOM` will return the beginning of the widget
* as the position for any click inside the widget.
* For callouts, this means that the task will never be found, since the `lineAt` will be the beginning of the callout.
* Therefore, produce an error message pop-up using Obsidian's "Notice" feature, log a console warning, then return.
*/

// Tasks from "task" query codeblocks handle themselves thanks to `toLi`, so be specific about error messaging, but still return.
const ancestor = target.closest(
'ul.plugin-tasks-query-result, div.callout-content',
);
if (ancestor) {
if (ancestor.matches('div.callout-content')) {
// Error message for now.
const msg =
'obsidian-tasks-plugin warning: Clicking a checkbox inside a callout in Live Preview not supported. \n' +
AnnaKornfeldSimpson marked this conversation as resolved.
Show resolved Hide resolved
'Undo your change, then either click the line of the task and use the "Toggle Task Done" command, or switch to Reading View to click the checkbox.';
console.warn(msg);
new Notice(msg, 30000);
}
return false;
}

const { state } = this.view;
const position = this.view.posAtDOM(target as Node);
const position = this.view.posAtDOM(target);
const line = state.doc.lineAt(position);
const task = Task.fromLine({
line: line.text,
Expand All @@ -47,6 +71,10 @@ class LivePreviewExtension implements PluginValue {
precedingHeader: null,
});

console.debug(
`Live Preview Extension: toggle called. Position: ${position} Line: ${line.text}`,
);

// Only handle checkboxes of tasks.
if (task === null) {
return false;
Expand All @@ -58,7 +86,7 @@ class LivePreviewExtension implements PluginValue {
// Clicked on a task's checkbox. Toggle the task and set it.
const toggled = task.toggle();
const toggledString = toggled
.map((task) => task.toFileLineString())
.map((t) => t.toFileLineString())
.join(state.lineBreak);

// Creates a CodeMirror transaction in order to update the document.
Expand Down
13 changes: 9 additions & 4 deletions src/Query/Filter/ExcludeSubItemsField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ export class ExcludeSubItemsField extends FilterInstructionsBasedField {
constructor() {
super();

this._filters.add(
'exclude sub-items',
(task) => task.indentation === '',
);
this._filters.add('exclude sub-items', (task) => {
if (task.indentation === '') return true; // no indentation, not a subitem

const lastBlockquoteMark = task.indentation.lastIndexOf('>');
if (lastBlockquoteMark === -1) return false; // indentation present, not in a blockquote, subitem

// Up to one space allowed after last > in blockquote/callout, otherwise subitem
return /^ ?$/.test(task.indentation.slice(lastBlockquoteMark + 1));
});
}

protected fieldName(): string {
Expand Down
4 changes: 2 additions & 2 deletions src/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ export class Task {
public static readonly dateFormat = 'YYYY-MM-DD';

// Main regex for parsing a line. It matches the following:
// - Indentation
// - Indentation (including > for potentially nested blockquotes or Obsidian callouts)
AnnaKornfeldSimpson marked this conversation as resolved.
Show resolved Hide resolved
// - Status character
// - Rest of task after checkbox markdown
public static readonly taskRegex = /^([\s\t]*)[-*] +\[(.)\] *(.*)/u;
public static readonly taskRegex = /^([\s\t>]*)[-*] +\[(.)\] *(.*)/u;

// Match on block link at end.
public static readonly blockLinkRegex = / \^[a-zA-Z0-9-]+$/u;
Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default class TasksPlugin extends Plugin {
vault: this.app.vault,
});

const events = new Events({ obsidianEents: this.app.workspace });
const events = new Events({ obsidianEvents: this.app.workspace });
this.cache = new Cache({
metadataCache: this.app.metadataCache,
vault: this.app.vault,
Expand Down
16 changes: 16 additions & 0 deletions tests/Query/Filter/ExcludeSubItemsField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,20 @@ describe('sub-items', () => {
testTaskFilter(filter, fromLine({ line: ' - [ ] Subtask1' }), false);
testTaskFilter(filter, fromLine({ line: ' - [ ] Subtask2' }), false);
});
it('subitem has more than one space after last > of blockquotes or callouts', () => {
// Arrange
const filter = new ExcludeSubItemsField().createFilterOrErrorMessage(
'exclude sub-items',
);

// Assert
testTaskFilter(filter, fromLine({ line: '> - [ ] Task' }), true);
testTaskFilter(filter, fromLine({ line: '> > - [ ] Task' }), true);
testTaskFilter(filter, fromLine({ line: '>> - [ ] Subtask1' }), false);
testTaskFilter(
filter,
fromLine({ line: '> > - [ ] Subtask2' }),
false,
);
});
});
57 changes: 56 additions & 1 deletion tests/Task.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,35 @@ describe('parsing', () => {
});
}
});

it('supports parsing of tasks inside blockquotes or callouts', () => {
// Arrange
const lines = [
'> - [ ] Task inside a blockquote or callout 📅2022-07-29',
'>>> - [ ] Task inside a blockquote or callout 📅2022-07-29',
'> > > * [ ] Task inside a blockquote or callout 📅2022-07-29',
];

// Act
for (const line of lines) {
const task = fromLine({
line,
});

// Assert
expect({
_input: line, // Line is included, so it is shown in any failure output
description: task.description,
due: task.dueDate?.format('YYYY-MM-DD'),
indentation: task.indentation,
}).toMatchObject({
_input: line,
description: 'Task inside a blockquote or callout',
due: '2022-07-29',
indentation: line.split(/[-*]/)[0],
});
AnnaKornfeldSimpson marked this conversation as resolved.
Show resolved Hide resolved
}
});
});

type TagParsingExpectations = {
Expand Down Expand Up @@ -295,7 +324,7 @@ describe('parsing tags', () => {
},
{
markdownTask:
'- [ ] Export [Cloud Feedly feeds](https://cloud.feedly.com/#opml) #context/pc_clare 🔁 every 4 weeks on Sunday ⏳ 2022-05-15 #context/more_context',
'* [ ] Export [Cloud Feedly feeds](https://cloud.feedly.com/#opml) #context/pc_clare 🔁 every 4 weeks on Sunday ⏳ 2022-05-15 #context/more_context',
expectedDescription:
'Export [Cloud Feedly feeds](https://cloud.feedly.com/#opml) #context/pc_clare #context/more_context',
extractedTags: ['#context/pc_clare', '#context/more_context'],
Expand All @@ -317,6 +346,20 @@ describe('parsing tags', () => {
extractedTags: ['#context/pc_clare'],
globalFilter: '',
},
{
markdownTask: '> - [ ] Task inside a blockquote or callout #tagone',
expectedDescription: 'Task inside a blockquote or callout #tagone',
extractedTags: ['#tagone'],
globalFilter: '',
},
{
markdownTask:
'>>> * [ ] Task inside a nested blockquote or callout #tagone',
expectedDescription:
'Task inside a nested blockquote or callout #tagone',
extractedTags: ['#tagone'],
globalFilter: '',
},
])(
'should parse "$markdownTask" and extract "$extractedTags"',
({
Expand Down Expand Up @@ -348,6 +391,18 @@ describe('parsing tags', () => {
});

describe('to string', () => {
it('retains the indentation', () => {
const line = '> > > - [ ] Task inside a nested blockquote or callout';

// Act
const task: Task = fromLine({
line,
}) as Task;

// Assert
expect(task).not.toBeNull();
expect(task.toFileLineString()).toStrictEqual(line);
});
it('retains the block link', () => {
// Arrange
const line = '- [ ] this is a task 📅 2021-09-12 ^my-precious';
Expand Down