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

feat: Toggle task done command adds global filter text, if enabled #2015

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Binary file modified docs/images/settings-global-filter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 43 additions & 5 deletions src/Commands/ToggleDone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ import { StatusRegistry } from '../StatusRegistry';

import { Task, TaskRegularExpressions } from '../Task';
import { TaskLocation } from '../TaskLocation';
import { GlobalFilter } from '../Config/GlobalFilter';

/**
* Storage for the line item line, broken down in to sections.
* See {@link extractLineItemComponents} for use.
*/
interface LineItemComponents {
indentation: string;
listMarker: string;
body: string;
}

export const toggleDone = (checking: boolean, editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
if (checking) {
Expand Down Expand Up @@ -82,16 +93,20 @@ export const toggleLine = (line: string, path: string): EditorInsertion => {
// 4. a standard task, but which does not contain the global filter, to be toggled, but no done date added.

// The task regex will match checklist items.
const regexMatch = line.match(TaskRegularExpressions.taskRegex);
if (regexMatch !== null) {
const checklistRegexMatch = line.match(TaskRegularExpressions.taskRegex);
const lineItemComponents = extractLineItemComponents(line);
if (checklistRegexMatch !== null) {
// Toggle the status of the checklist item.
const statusString = regexMatch[3];
const statusString = checklistRegexMatch[3];
const status = StatusRegistry.getInstance().bySymbol(statusString);
const newStatusString = status.nextStatusSymbol;
return { text: line.replace(TaskRegularExpressions.taskRegex, `$1- [${newStatusString}] $4`) };
} else if (TaskRegularExpressions.listItemRegex.test(line)) {
} else if (lineItemComponents) {
const { indentation, listMarker, body } = lineItemComponents;
// Convert the list item to a checklist item.
const text = line.replace(TaskRegularExpressions.listItemRegex, '$1$2 [ ]');
const newBody = GlobalFilter.addGlobalFilterToDescriptionDependingOnSettings(body);

const text = `${indentation}${listMarker} [ ] ${newBody}`;
return { text, moveTo: { ch: text.length } };
} else {
// Convert the line to a list item.
Expand Down Expand Up @@ -129,3 +144,26 @@ export const getNewCursorPosition = (startPos: EditorPosition, insertion: Editor
ch: Math.min(moveTo.ch, destinationLineLength),
};
};

/**
* Uses a regex to extract out the components of a list item
*
* Returns `null` if the line does not match
*
* @param line a line like `- write blog post`
* */
function extractLineItemComponents(line: string): LineItemComponents | null {
// Check the line to see if it is a markdown list item.
const regexMatch = line.match(TaskRegularExpressions.listItemRegex);
if (regexMatch === null) {
return null;
}

const indentation = regexMatch[1];
const listMarker = regexMatch[2];

// match[3] includes the whole body of the list item after the list marker.
const body = regexMatch[3].trim();

return { indentation, listMarker, body };
}
14 changes: 14 additions & 0 deletions src/Config/GlobalFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ export class GlobalFilter {
return GlobalFilter.get() + ' ' + description;
}

static addGlobalFilterToDescriptionDependingOnSettings(description: string): string {
if (GlobalFilter.shouldAddGlobalFilter(description)) {
return GlobalFilter.prependTo(description);
} else {
return description;
}
}

private static shouldAddGlobalFilter(description: string): boolean {
const { autoInsertGlobalFilter } = getSettings();

return !GlobalFilter.isEmpty() && autoInsertGlobalFilter && !GlobalFilter.includedIn(description);
}

static removeAsWordFromDependingOnSettings(description: string): string {
const { removeGlobalFilter } = getSettings();
if (removeGlobalFilter) {
Expand Down
2 changes: 2 additions & 0 deletions src/Config/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export type TASK_FORMATS = typeof TASK_FORMATS; // For convenience to make some
export interface Settings {
globalQuery: string;
globalFilter: string;
autoInsertGlobalFilter: boolean;
removeGlobalFilter: boolean;
taskFormat: keyof TASK_FORMATS;
setCreatedDate: boolean;
Expand Down Expand Up @@ -82,6 +83,7 @@ export interface Settings {
const defaultSettings: Settings = {
globalQuery: '',
globalFilter: GlobalFilter.empty,
autoInsertGlobalFilter: false,
removeGlobalFilter: false,
taskFormat: 'tasksPluginEmoji',
setCreatedDate: false,
Expand Down
17 changes: 17 additions & 0 deletions src/Config/SettingsTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,23 @@ export class SettingsTab extends PluginSettingTab {
});
});

new Setting(containerEl)
.setName('"Tasks: Toggle task done" command inserts global task filter')
.setDesc(
SettingsTab.createFragmentWithHTML(
'Enabling this causes "Tasks: Toggle task done" command to insert the global task filter when creating a new checkbox',
),
)
.addToggle((toggle) => {
const settings = getSettings();

toggle.setValue(settings.autoInsertGlobalFilter).onChange(async (value) => {
updateSettings({ autoInsertGlobalFilter: value });

await this.plugin.saveSettings();
});
});

new Setting(containerEl)
.setName('Remove global filter from description')
.setDesc(
Expand Down
4 changes: 3 additions & 1 deletion src/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ export class TaskRegularExpressions {

// Used with "Toggle Done" command to detect a list item that can get a checkbox added to it.
public static readonly listItemRegex = new RegExp(
TaskRegularExpressions.indentationRegex.source + TaskRegularExpressions.listMarkerRegex.source,
TaskRegularExpressions.indentationRegex.source +
TaskRegularExpressions.listMarkerRegex.source +
TaskRegularExpressions.afterCheckboxRegex.source,
);

// Match on block link at end.
Expand Down
9 changes: 8 additions & 1 deletion src/TaskSerializer/DefaultTaskSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,14 @@ export class DefaultTaskSerializer implements TaskSerializer {
// components but now we want them back.
// The goal is for a task of them form 'Do something #tag1 (due) tomorrow #tag2 (start) today'
// to actually have the description 'Do something #tag1 #tag2'
if (trailingTags.length > 0) line += ' ' + trailingTags;
if (trailingTags.length > 0) {
// If the line is empty besides the tag then don't prepend a space because it results in a double space
if (line === '') {
line += trailingTags;
} else {
line += ' ' + trailingTags;
}
}

return {
description: line,
Expand Down
156 changes: 138 additions & 18 deletions tests/Commands/ToggleDone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { GlobalFilter } from '../../src/Config/GlobalFilter';
import { StatusRegistry } from '../../src/StatusRegistry';
import { Status } from '../../src/Status';
import { StatusConfiguration } from '../../src/StatusConfiguration';
import { updateSettings } from '../../src/Config/Settings';

window.moment = moment;

Expand Down Expand Up @@ -79,6 +80,7 @@ function testToggleLineForOutOfRangeCursorPositions(
describe('ToggleDone', () => {
afterEach(() => {
GlobalFilter.reset();
updateSettings({ autoInsertGlobalFilter: false });
});

const todaySpy = jest.spyOn(Date, 'now').mockReturnValue(moment('2022-09-04').valueOf());
Expand All @@ -99,32 +101,150 @@ describe('ToggleDone', () => {
testToggleLine('foo|bar', '- foobar|');
});

it('should add checkbox to hyphen and space', () => {
testToggleLine('|- ', '- [ ] |');
testToggleLine('- |', '- [ ] |');
testToggleLine('- |foobar', '- [ ] foobar|');
describe('should add checkbox to hyphen and space', () => {
it('if autoInsertGlobalFilter is false, then an empty global filter is not added', () => {
GlobalFilter.set('');
updateSettings({ autoInsertGlobalFilter: false });

GlobalFilter.set('#task');
testToggleLine('|- ', '- [ ] |');
testToggleLine('- |', '- [ ] |');
testToggleLine('- |foobar', '- [ ] foobar|');
});

it('if autoInsertGlobalFilter is true, then and empty global filter is not added', () => {
GlobalFilter.set('');
updateSettings({ autoInsertGlobalFilter: true });

testToggleLine('|- ', '- [ ] |');
testToggleLine('- |', '- [ ] |');
testToggleLine('- |foobar', '- [ ] foobar|');
});

it('if autoInsertGlobalFilter is false, then a tag global filter is not added', () => {
GlobalFilter.set('#task');
updateSettings({ autoInsertGlobalFilter: false });

testToggleLine('|- ', '- [ ] |');
testToggleLine('- |', '- [ ] |');
testToggleLine('- |foobar', '- [ ] foobar|');
testToggleLine('- |#task', '- [ ] #task|');
testToggleLine('- |write blog post #task', '- [ ] write blog post #task|');
});

it('if autoInsertGlobalFilter is true, then a tag global filter is added if absent', () => {
GlobalFilter.set('#task');
updateSettings({ autoInsertGlobalFilter: true });

testToggleLine('|- ', '- [ ] #task |');
testToggleLine('- |', '- [ ] #task |');
testToggleLine('- |foobar', '- [ ] #task foobar|');
testToggleLine('- |#task', '- [ ] #task|');
testToggleLine('- |write blog post #task', '- [ ] write blog post #task|');
});

it('if autoInsertGlobalFilter is false, then a non-tag global filter is not added', () => {
GlobalFilter.set('TODO');
updateSettings({ autoInsertGlobalFilter: false });

testToggleLine('|- ', '- [ ] |');
testToggleLine('- |', '- [ ] |');
testToggleLine('- |foobar', '- [ ] foobar|');
testToggleLine('- |TODO foobar', '- [ ] TODO foobar|');
testToggleLine('- |write blog post TODO', '- [ ] write blog post TODO|');
});

it('if autoInsertGlobalFilter is true, then a non-tag global filter is added if absent', () => {
GlobalFilter.set('TODO');
updateSettings({ autoInsertGlobalFilter: true });

testToggleLine('|- ', '- [ ] TODO |');
testToggleLine('- |', '- [ ] TODO |');
testToggleLine('- |foobar', '- [ ] TODO foobar|');
testToggleLine('- |TODO foobar', '- [ ] TODO foobar|');
testToggleLine('- |write blog post TODO', '- [ ] write blog post TODO|');
});

it('regex global filter is not broken', () => {
// Test a global filter that has special characters from regular expressions
// if autoInsertGlobalFilter is false, then global filter is not added
GlobalFilter.set('a.*b');
updateSettings({ autoInsertGlobalFilter: false });

testToggleLine('|- ', '- [ ] |');
testToggleLine('- |', '- [ ] |');
testToggleLine('- |foobar', '- [ ] foobar|');
testToggleLine('|- [ ] a.*b ', '|- [x] a.*b ✅ 2022-09-04');
testToggleLine('- [ ] a.*b foobar |', '- [x] a.*b foobar |✅ 2022-09-04');

GlobalFilter.set('a.*b');
updateSettings({ autoInsertGlobalFilter: true });

testToggleLine('|- [ ] a.*b ', '|- [x] a.*b ✅ 2022-09-04');
testToggleLine('- [ ] a.*b foobar |', '- [x] a.*b foobar |✅ 2022-09-04');
});
});

it('should complete a task', () => {
testToggleLine('|- [ ] ', '|- [x] ✅ 2022-09-04');
testToggleLine('- [ ] |', '- [x] | ✅ 2022-09-04');
describe('should complete a task', () => {
it('when completing a task without a global filter', () => {
testToggleLine('|- [ ] ', '|- [x] ✅ 2022-09-04');
testToggleLine('- [ ] |', '- [x] | ✅ 2022-09-04');
testToggleLine('- [ ]| ', '- [x]| ✅ 2022-09-04');

// Issue #449 - cursor jumped 13 characters to the right on completion
testToggleLine('- [ ] I have a |proper description', '- [x] I have a |proper description ✅ 2022-09-04');
// Issue #449 - cursor jumped 13 characters to the right on completion
testToggleLine('- [ ] I have a |proper description', '- [x] I have a |proper description ✅ 2022-09-04');
});

GlobalFilter.set('#task');
it('when completing a task with a tag global filter', () => {
GlobalFilter.set('#task');

const completesWithTaskGlobalFilter = () => {
testToggleLine('|- [ ] ', '|- [x] ');
testToggleLine('- [ ] |', '- [x] |');

testToggleLine('|- [ ] #task ', '|- [x] #task ✅ 2022-09-04');
testToggleLine('- [ ] #task foobar |', '- [x] #task foobar |✅ 2022-09-04');
};

testToggleLine('|- [ ] ', '|- [x] ');
testToggleLine('- [ ] |', '- [x] |');
updateSettings({ autoInsertGlobalFilter: true });
completesWithTaskGlobalFilter();
updateSettings({ autoInsertGlobalFilter: false });
completesWithTaskGlobalFilter();

// Issue #449 - cursor jumped 13 characters to the right on completion
testToggleLine('- [ ] I have a |proper description', '- [x] I have a |proper description');
});

// Issue #449 - cursor jumped 13 characters to the right on completion
testToggleLine('- [ ] I have a |proper description', '- [x] I have a |proper description');
it('when completing a task with a non-tag global filter', () => {
GlobalFilter.set('TODO');

const completesWithTodoGlobalFilter = () => {
testToggleLine('|- [ ] ', '|- [x] ');
testToggleLine('- [ ] |', '- [x] |');

testToggleLine('|- [ ] TODO ', '|- [x] TODO ✅ 2022-09-04');
testToggleLine('- [ ] TODO foobar |', '- [x] TODO foobar |✅ 2022-09-04');
};

updateSettings({ autoInsertGlobalFilter: true });
completesWithTodoGlobalFilter();
updateSettings({ autoInsertGlobalFilter: false });
completesWithTodoGlobalFilter();
});

it('when completing a task with a regex global filter', () => {
// Test a global filter that has special characters from regular expressions
GlobalFilter.set('a.*b');

const completesWithRegexGlobalFilter = () => {
testToggleLine('|- [ ] ', '|- [x] ');
testToggleLine('- [ ] |', '- [x] |');

testToggleLine('|- [ ] a.*b ', '|- [x] a.*b ✅ 2022-09-04');
testToggleLine('- [ ] a.*b foobar |', '- [x] a.*b foobar |✅ 2022-09-04');
};

updateSettings({ autoInsertGlobalFilter: true });
completesWithRegexGlobalFilter();
updateSettings({ autoInsertGlobalFilter: false });
completesWithRegexGlobalFilter();
});
});

it('should un-complete a completed task', () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/TaskSerializer/DataviewTaskSerializer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ describe('DataviewTaskSerializer', () => {
});

it('should parse tags', () => {
const description = ' #hello #world #task';
const description = '#hello #world #task';
const taskDetails = deserialize(description);
expect(taskDetails).toMatchTaskDetails({ tags: ['#hello', '#world', '#task'], description });
});
Expand Down
2 changes: 1 addition & 1 deletion tests/TaskSerializer/DefaultTaskSerializer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe.each(symbolMap)("DefaultTaskSerializer with '$taskFormat' symbols", ({
});

it('should parse tags', () => {
const description = ' #hello #world #task';
const description = '#hello #world #task';
const taskDetails = deserialize(description);
expect(taskDetails).toMatchTaskDetails({ tags: ['#hello', '#world', '#task'], description });
});
Expand Down