|
| 1 | +// @flow |
| 2 | +//----------------------------------------------------------------------------- |
| 3 | +// Main functions for Tidy plugin |
| 4 | +// Jonathan Clark |
| 5 | +// Last updated 2025-06-24 for v0.14.8, @jgclark |
| 6 | +//----------------------------------------------------------------------------- |
| 7 | + |
| 8 | +import pluginJson from '../plugin.json' |
| 9 | +import { moveTopLevelTasksInNote } from './topLevelTasks' |
| 10 | +import { getSettings, type TidyConfig } from './tidyHelpers' |
| 11 | +import { clo, JSP, logDebug, logError, logInfo, logWarn, overrideSettingsWithEncodedTypedArgs, timer } from '@helpers/dev' |
| 12 | +import { displayTitle, getTagParamsFromString } from '@helpers/general' |
| 13 | +import { allNotesSortedByChanged, pastCalendarNotes, removeSection } from '@helpers/note' |
| 14 | +import { getNotesChangedInIntervalFromList } from '@helpers/NPnote' |
| 15 | +import { findHeading, findHeadingInNotes, removeContentUnderHeadingInAllNotes } from '@helpers/NPParagraph' |
| 16 | +import { getInputTrimmed, showMessage, showMessageYesNo } from '@helpers/userInput' |
| 17 | + |
| 18 | +//----------------------------------------------------------------------------- |
| 19 | + |
| 20 | +/** |
| 21 | + * Remove a given section (by matching on their section heading) from recently-changed Notes. Note: does not match on note title. |
| 22 | + * Can be passed parameters to override default time interval through an x-callback call. |
| 23 | + * @author @jgclark |
| 24 | + * @param {?string} params optional JSON string |
| 25 | + */ |
| 26 | +export async function removeSectionFromRecentNotes(params: string = ''): Promise<void> { |
| 27 | + try { |
| 28 | + // Get plugin settings (config) |
| 29 | + let config: TidyConfig = await getSettings() |
| 30 | + // Setup main variables |
| 31 | + if (params) { |
| 32 | + logDebug(pluginJson, `removeSectionFromRecentNotes: Starting with params '${params}'`) |
| 33 | + config = overrideSettingsWithEncodedTypedArgs(config, params) |
| 34 | + clo(config, `config after overriding with params '${params}'`) |
| 35 | + } else { |
| 36 | + // If no params are passed, then we've been called by a plugin command (and so use defaults from config). |
| 37 | + logDebug(pluginJson, `removeSectionFromRecentNotes: Starting with no params`) |
| 38 | + } |
| 39 | + |
| 40 | + // Get num days to process from param, or by asking user if necessary |
| 41 | + const numDays: number = await getTagParamsFromString(params ?? '', 'numDays', config.numDays || 0) |
| 42 | + logDebug('removeSectionFromRecentNotes', `numDays = ${String(numDays)}`) |
| 43 | + // Note: can be 0 at this point, which implies process all days |
| 44 | + |
| 45 | + // Decide whether to run silently |
| 46 | + const runSilently: boolean = await getTagParamsFromString(params ?? '', 'runSilently', false) |
| 47 | + logDebug('removeSectionFromRecentNotes', `runSilently = ${String(runSilently)}`) |
| 48 | + |
| 49 | + // Decide what matching type to use |
| 50 | + const matchType: string = await getTagParamsFromString(params ?? '', 'matchType', config.matchType) |
| 51 | + logDebug('removeSectionFromRecentNotes', `matchType = ${matchType}`) |
| 52 | + |
| 53 | + // If not passed as a parameter already, ask for section heading to remove |
| 54 | + let sectionHeading: string = await getTagParamsFromString(params ?? '', 'sectionHeading', '') |
| 55 | + if (sectionHeading === '') { |
| 56 | + const res: string | boolean = await getInputTrimmed(`What's the heading of the section you'd like to remove from ${numDays > 0 ? 'some' : 'all'} notes?`, 'OK', 'Remove Section from Notes') |
| 57 | + if (res === false) { |
| 58 | + return |
| 59 | + } else { |
| 60 | + sectionHeading = String(res) // to help flow |
| 61 | + } |
| 62 | + } |
| 63 | + logDebug('removeSectionFromRecentNotes', `sectionHeading = ${sectionHeading}`) |
| 64 | + |
| 65 | + // Find which notes have such a section to remove |
| 66 | + // Find notes with matching heading (or speed, let's multi-core search the notes to find the notes that contain this string) |
| 67 | + let allMatchedParas: $ReadOnlyArray<TParagraph> = await DataStore.search(sectionHeading, ['calendar', 'notes'], [], config.removeFoldersToExclude) |
| 68 | + // This returns all the potential matches, but some may not be headings, so now check for those |
| 69 | + switch (matchType) { |
| 70 | + case 'Exact': |
| 71 | + allMatchedParas = allMatchedParas.filter((n) => n.type === 'title' && n.content === sectionHeading && n.headingLevel !== 1) |
| 72 | + break |
| 73 | + case 'Starts with': |
| 74 | + allMatchedParas = allMatchedParas.filter((n) => n.type === 'title' && n.content.startsWith(sectionHeading) && n.headingLevel !== 1) |
| 75 | + break |
| 76 | + case 'Contains': |
| 77 | + allMatchedParas = allMatchedParas.filter((n) => n.type === 'title' && n.content.includes(sectionHeading) && n.headingLevel !== 1) |
| 78 | + } |
| 79 | + let numToRemove = allMatchedParas.length |
| 80 | + const allMatchedNotes = allMatchedParas.map((p) => p.note) |
| 81 | + logDebug('removeSectionFromRecentNotes', `- ${String(numToRemove)} matches of '${sectionHeading}' as heading from ${String(allMatchedNotes.length)} notes`) |
| 82 | + |
| 83 | + // Now keep only those changed recently (or all if numDays === 0) |
| 84 | + // $FlowFixMe[incompatible-type] |
| 85 | + const notesToProcess: Array<TNote> = numDays > 0 ? getNotesChangedInIntervalFromList(allMatchedNotes.filter(Boolean), numDays) : allMatchedNotes |
| 86 | + numToRemove = notesToProcess.length |
| 87 | + |
| 88 | + if (numToRemove > 0) { |
| 89 | + logDebug('removeSectionFromRecentNotes', `- ${String(numToRemove)} are in the right date interval:`) |
| 90 | + const titlesList = notesToProcess.map((m) => displayTitle(m)) |
| 91 | + logDebug('removeSectionFromRecentNotes', titlesList) |
| 92 | + // Check user wants to proceed (if not calledWithParams) |
| 93 | + if (!runSilently) { |
| 94 | + const res = await showMessageYesNo(`Do you want to remove ${String(numToRemove)} '${sectionHeading}' sections?`, ['Yes', 'No'], 'Remove Section from Notes') |
| 95 | + if (res !== 'Yes') { |
| 96 | + logInfo('removeSectionFromRecentNotes', `User cancelled operation`) |
| 97 | + return |
| 98 | + } |
| 99 | + } |
| 100 | + // Actually remove those sections |
| 101 | + for (const note of notesToProcess) { |
| 102 | + logDebug('removeSectionFromRecentNotes', `- Removing section in note '${displayTitle(note)}'`) |
| 103 | + // const lineNum = |
| 104 | + removeSection(note, sectionHeading) |
| 105 | + } |
| 106 | + } else { |
| 107 | + if (!runSilently) { |
| 108 | + const res = await showMessage(`No sections with heading '${sectionHeading}' were found to remove`) |
| 109 | + } |
| 110 | + logInfo('removeSectionFromRecentNotes', `No sections with heading '${sectionHeading}' were found to remove`) |
| 111 | + } |
| 112 | + |
| 113 | + return |
| 114 | + } catch (err) { |
| 115 | + logError('removeSectionFromRecentNotes', err.message) |
| 116 | + return // for completeness |
| 117 | + } |
| 118 | +} |
| 119 | + |
| 120 | +/** |
| 121 | + * WARNING: Dangerous! Remove a given section from all Notes. |
| 122 | + * Can be passed parameters to override default settings. |
| 123 | + * @author @jgclark wrapping function by @dwertheimer |
| 124 | + * @param {?string} params optional JSON string |
| 125 | + */ |
| 126 | +export async function removeSectionFromAllNotes(params: string = ''): Promise<void> { |
| 127 | + try { |
| 128 | + // Get plugin settings (config) |
| 129 | + let config: TidyConfig = await getSettings() |
| 130 | + // Setup main variables |
| 131 | + if (params) { |
| 132 | + logDebug(pluginJson, `removeSectionFromAllNotes: Starting with params '${params}'`) |
| 133 | + config = overrideSettingsWithEncodedTypedArgs(config, params) |
| 134 | + clo(config, `config after overriding with params '${params}'`) |
| 135 | + } else { |
| 136 | + // If no params are passed, then we've been called by a plugin command (and so use defaults from config). |
| 137 | + logDebug(pluginJson, `removeSectionFromAllNotes: Starting with no params`) |
| 138 | + } |
| 139 | + |
| 140 | + // Decide whether to run silently, using parameter if given |
| 141 | + const runSilently: boolean = await getTagParamsFromString(params ?? '', 'runSilently', false) |
| 142 | + logDebug('removeDoneMarkers', `runSilently: ${String(runSilently)}`) |
| 143 | + // We also need a string version of this for legacy reasons |
| 144 | + const runSilentlyAsString: string = runSilently ? 'yes' : 'no' |
| 145 | + |
| 146 | + // Decide whether to keep heading, using parameter if given |
| 147 | + const keepHeading: boolean = await getTagParamsFromString(params ?? '', 'keepHeading', false) |
| 148 | + |
| 149 | + // If not passed as a parameter already, ask for section heading to remove |
| 150 | + let sectionHeading: string = await getTagParamsFromString(params ?? '', 'sectionHeading', '') |
| 151 | + if (sectionHeading === '') { |
| 152 | + const res: string | boolean = await getInputTrimmed("What's the heading of the section you'd like to remove from ALL notes?", 'OK', 'Remove Section from Notes') |
| 153 | + if (res === false) { |
| 154 | + return |
| 155 | + } else { |
| 156 | + sectionHeading = String(res) // to help flow |
| 157 | + } |
| 158 | + } |
| 159 | + logDebug('removeSectionFromAllNotes', `sectionHeading: '${sectionHeading}'`) |
| 160 | + logDebug('removeSectionFromAllNotes', `matchType: '${config.matchType}'`) |
| 161 | + logDebug('removeSectionFromAllNotes', `removeFoldersToExclude: '${String(config.removeFoldersToExclude)}'`) |
| 162 | + |
| 163 | + // Now see how many matching headings there are |
| 164 | + let parasToRemove = await findHeadingInNotes(sectionHeading, config.matchType, config.removeFoldersToExclude, true) |
| 165 | + // Ideally work out how many this will remove, and then use this code: |
| 166 | + if (parasToRemove.length > 0) { |
| 167 | + if (!runSilently) { |
| 168 | + const res = await showMessageYesNo(`Are you sure you want to remove ${String(parasToRemove.length)} '${sectionHeading}' sections? (See Plugin Console for full list)`, ['Yes', 'No'], 'Remove Section from Notes') |
| 169 | + if (res === 'No') { |
| 170 | + logInfo('removeSectionFromAllNotes', `User cancelled operation`) |
| 171 | + return |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | + // Run the powerful removal function by @dwertheimer |
| 176 | + removeContentUnderHeadingInAllNotes(['Calendar', 'Notes'], sectionHeading, keepHeading, runSilentlyAsString) |
| 177 | + logInfo(pluginJson, `Removed '${sectionHeading}' sections from all notes`) |
| 178 | + |
| 179 | + } else { |
| 180 | + if (!runSilently) { |
| 181 | + logInfo(pluginJson, `No sections with sectionHeading '${sectionHeading}' were found to remove`) |
| 182 | + const res = await showMessage(`No sections with heading '${sectionHeading}' were found to remove`) |
| 183 | + } else { |
| 184 | + logDebug(pluginJson, `No sections with sectionHeading '${sectionHeading}' were found to remove`) |
| 185 | + } |
| 186 | + } |
| 187 | + return |
| 188 | + } catch (err) { |
| 189 | + logError('removeSectionFromAllNotes', JSP(err)) |
| 190 | + return // for completeness |
| 191 | + } |
| 192 | +} |
0 commit comments