diff --git a/docs/images/settings-global-filter.png b/docs/images/settings-global-filter.png index 871490edb5..fee8134692 100644 Binary files a/docs/images/settings-global-filter.png and b/docs/images/settings-global-filter.png differ diff --git a/src/Commands/ToggleDone.ts b/src/Commands/ToggleDone.ts index 62c9943110..3fdd7915c5 100644 --- a/src/Commands/ToggleDone.ts +++ b/src/Commands/ToggleDone.ts @@ -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) { @@ -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. @@ -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 }; +} diff --git a/src/Config/GlobalFilter.ts b/src/Config/GlobalFilter.ts index 9158235a7b..e12c673242 100644 --- a/src/Config/GlobalFilter.ts +++ b/src/Config/GlobalFilter.ts @@ -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) { diff --git a/src/Config/Settings.ts b/src/Config/Settings.ts index bc71f2b265..ab160a4741 100644 --- a/src/Config/Settings.ts +++ b/src/Config/Settings.ts @@ -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; @@ -82,6 +83,7 @@ export interface Settings { const defaultSettings: Settings = { globalQuery: '', globalFilter: GlobalFilter.empty, + autoInsertGlobalFilter: false, removeGlobalFilter: false, taskFormat: 'tasksPluginEmoji', setCreatedDate: false, diff --git a/src/Config/SettingsTab.ts b/src/Config/SettingsTab.ts index 6427384e3c..a2bd9354d3 100644 --- a/src/Config/SettingsTab.ts +++ b/src/Config/SettingsTab.ts @@ -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( diff --git a/src/Task.ts b/src/Task.ts index 06160dde0d..4f189567fa 100644 --- a/src/Task.ts +++ b/src/Task.ts @@ -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. diff --git a/src/TaskSerializer/DefaultTaskSerializer.ts b/src/TaskSerializer/DefaultTaskSerializer.ts index b0c57d2465..d1d4cfad96 100644 --- a/src/TaskSerializer/DefaultTaskSerializer.ts +++ b/src/TaskSerializer/DefaultTaskSerializer.ts @@ -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, diff --git a/tests/Commands/ToggleDone.test.ts b/tests/Commands/ToggleDone.test.ts index 1d762cf441..d5534907b3 100644 --- a/tests/Commands/ToggleDone.test.ts +++ b/tests/Commands/ToggleDone.test.ts @@ -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; @@ -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()); @@ -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', () => { diff --git a/tests/TaskSerializer/DataviewTaskSerializer.test.ts b/tests/TaskSerializer/DataviewTaskSerializer.test.ts index 16ecb5269c..a4d0d32d5f 100644 --- a/tests/TaskSerializer/DataviewTaskSerializer.test.ts +++ b/tests/TaskSerializer/DataviewTaskSerializer.test.ts @@ -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 }); }); diff --git a/tests/TaskSerializer/DefaultTaskSerializer.test.ts b/tests/TaskSerializer/DefaultTaskSerializer.test.ts index ecc202098e..7f11cdf051 100644 --- a/tests/TaskSerializer/DefaultTaskSerializer.test.ts +++ b/tests/TaskSerializer/DefaultTaskSerializer.test.ts @@ -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 }); });