Skip to content

Commit 22b6e78

Browse files
committed
Merge branch 'main' of https://github.com/NotePlan/plugins
2 parents 142781f + 93f587a commit 22b6e78

File tree

8 files changed

+257
-185
lines changed

8 files changed

+257
-185
lines changed

flow-typed/Noteplan.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -774,8 +774,8 @@ declare class DataStore {
774774
* Searches all notes for a keyword (uses multiple threads to speed it up).
775775
* By default it searches in project notes and in the calendar notes. Use the second parameters "typesToInclude" to include specific types. Otherwise, pass `null` or nothing to include all of them.
776776
* This function is async, use it with `await`, so that the UI is not being blocked during a long search.
777-
* Optionally pass a list of folders (`inNotes`) to limit the search to notes that ARE in those folders (applies only to project notes). If empty, it is ignored.
778-
* Optionally pass a list of folders (`notInFolders`) to limit the search to notes NOT in those folders (applies only to project notes). If empty, it is ignored.
777+
* Optionally pass a list of folders (`inFolders`) to limit the search to notes that ARE in those folders (applies only to project notes). If empty, it is ignored.
778+
* Optionally pass a list of folders (`notInFolderslist`) to limit the search to notes NOT in those folders (applies only to project notes). If empty, it is ignored.
779779
* Searches for keywords are case-insensitive.
780780
* It will sort it by filename (so search results from the same notes stay together) and calendar notes also by filename with the newest at the top (highest dates).
781781
* Note: Available from v3.6.0

helpers/NPParagraph.js

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ import {
2525
import { displayTitle } from '@helpers/general'
2626
import { getFirstDateInPeriod, getNPWeekData, getMonthData, getQuarterData, getYearData, nowDoneDateTimeString, toLocaleDateTimeString } from '@helpers/NPdateTime'
2727
import { clo, JSP, logDebug, logError, logInfo, logWarn, timer } from '@helpers/dev'
28-
import { getNoteType } from '@helpers/note'
28+
import { filterOutParasInExcludeFolders, getNoteType } from '@helpers/note'
2929
import { findStartOfActivePartOfNote, isTermInMarkdownPath, isTermInURL } from '@helpers/paragraph'
3030
import { RE_FIRST_SCHEDULED_DATE_CAPTURE } from '@helpers/regex'
31-
import { getLineMainContentPos } from '@helpers/search'
31+
import { caseInsensitiveMatch, caseInsensitiveSubstringMatch, caseInsensitiveStartsWith, getLineMainContentPos } from '@helpers/search'
3232
import { stripTodaysDateRefsFromString } from '@helpers/stringTransforms'
3333
import { hasScheduledDate, isOpen, isOpenAndScheduled } from '@helpers/utils'
3434

@@ -498,14 +498,60 @@ export async function blockContainsOnlySyncedCopies(note: CoreNoteFields, showEr
498498
return true
499499
}
500500
501+
/**
502+
* Find given heading in all notes of type 'noteTypes', unless its in 'foldersToExclude'. Returns array of paragraphs.
503+
* @author @jgclark
504+
* @param {string} heading
505+
* @param {string?} matchMode - 'Exact', 'Contains' (default) or 'Starts with'
506+
* @param {Array<string>?} excludedFolders - array of folder names to exclude/ignore (if a file is in one of these folders, it will be removed)
507+
* @param {boolean} includeCalendar? - whether to include Calendar notes (default: true)
508+
* @returns {Array<TParagraph>}
509+
*/
510+
export async function findHeadingInNotes(
511+
heading: string,
512+
matchMode: string = 'contains',
513+
excludedFolders: Array<string> = [],
514+
includeCalendar: boolean = true
515+
): Promise<Array<TParagraph>> {
516+
// For speed, let's first multi-core search the notes to find the notes that contain this string
517+
const noteTypes = includeCalendar ? ['notes', 'calendar'] : ['notes']
518+
const initialParasList = await DataStore.search(heading, noteTypes, [], excludedFolders) // returns all the potential matches, but some may not be headings
519+
logDebug('findHeadingInNotes', `'Finding ${heading}' with mode '${matchMode}'`)
520+
logDebug('findHeadingInNotes', `- initially found ${String(initialParasList.length)} heading paras`)
521+
let filteredParas: Array<TParagraph> = []
522+
switch (matchMode) {
523+
case 'Exact': {
524+
filteredParas = initialParasList
525+
.filter((p) => p.type === 'title' && caseInsensitiveMatch(heading, p.content))
526+
break
527+
}
528+
case 'Starts with': {
529+
filteredParas = initialParasList
530+
.filter((p) => p.type === 'title' && caseInsensitiveStartsWith(heading, p.content, false))
531+
break
532+
}
533+
default: { // 'Contains'
534+
filteredParas = initialParasList
535+
.filter((p) => p.type === 'title' && caseInsensitiveSubstringMatch(heading, p.content))
536+
break
537+
}
538+
}
539+
logDebug(`removeSectionFromAllNotes`, `- list of ${String(filteredParas.length)} notes/section headings found:`)
540+
for (const p of filteredParas) {
541+
logDebug('', `- in '${displayTitle(p.note)}': '${String(p.content)}'`)
542+
}
543+
return filteredParas
544+
}
545+
501546
/**
502547
* Remove all previously written blocks under a given heading in all notes (e.g. for deleting previous "TimeBlocks" or "SyncedCopies")
503548
* WARNING: This is DANGEROUS. Could delete a lot of content. You have been warned!
504549
* @author @dwertheimer
505550
* @param {Array<string>} noteTypes - the types of notes to look in -- e.g. ['calendar','notes']
506-
* @param {string} heading - the heading too look for in the notes (without the #)
551+
* @param {string} heading - the heading to look for in the notes (without the #)
507552
* @param {boolean} keepHeading - whether to leave the heading in place afer all the content underneath is
508553
* @param {boolean} runSilently - whether to show CommandBar popups confirming how many notes will be affected - you should set it to 'yes' when running from a template
554+
* @param {boolean?} syncedOnly?
509555
*/
510556
export async function removeContentUnderHeadingInAllNotes(
511557
noteTypes: Array<string>,

np.Tidy/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# 🧹 Tidy Up Changelog
22
See Plugin [README](https://github.com/NotePlan/plugins/blob/main/np.Tidy/README.md) for full details on the available commands and use from callbacks and templates.
33

4+
## [0.14.8] - 2025-06-24 @jgclark
5+
- updated **Remove section from all notes** command to show how many sections it will remove, and also to use the 'Type of match for section headings' (`Exact`, `Starts with`, or `Contains`) and 'Folders to exclude' settings
6+
- code refactoring
7+
48
## [0.14.7] - 2025-02-18 @jgclark
59
- Stop lots of popups appearing when running **/Generate @repeats in recent notes** command (thanks, @kanera).
610
- The **/List stubs** command now understands line links (and so ignores the part of the link after the `^` character) (thanks, @ChrisMetcalf).

np.Tidy/plugin.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
"plugin.name": "🧹 Tidy Up",
77
"plugin.author": "jgclark",
88
"plugin.description": "Tidy up and delete various things in your NotePlan notes",
9-
"plugin.version": "0.14.7",
10-
"plugin.lastUpdateInfo": "v0.14.7: fix regression in '/generate @repeats from recent notes' command.\nv0.14.6: fix to allow top-level tasks to be run by xcallback.\nv0.14.5: fix to allow top-level tasks to be run by xcallback.\nv0.14.4: fix to allow blank Calendar notes to be removed by '/remove blank notes'.\nv0.14.3: fix unwanted popups in '/generate @repeats from recent notes' command.\nv0.14.2: add new option to file root notes.\nv0.14.1: rebuild.\nv0.14.0: new '/Generate repeats' command.\nv0.13.0: '/List conflicted notes' now clears out all copies, and offers side-by-side viewing of conflicted note versions. Also bug fixes.\nv0.12.1: '/List conflicted notes' now covers Calendar notes as well.\nv0.12.0: add more capability to '/List conflicted notes'.\nv0.11.0: new command '/find doubled notes'.\nv0.10.0: fix bug in moving top level tasks, and adds support for indented tasks.",
9+
"plugin.version": "0.14.8",
10+
"plugin.lastUpdateInfo": "v0.14.8: improvements to '/Remove section from all notes' command.\nv0.14.7: fix regression in '/generate @repeats from recent notes' command.\nv0.14.6: fix to allow top-level tasks to be run by xcallback.\nv0.14.5: fix to allow top-level tasks to be run by xcallback.\nv0.14.4: fix to allow blank Calendar notes to be removed by '/remove blank notes'.\nv0.14.3: fix unwanted popups in '/generate @repeats from recent notes' command.\nv0.14.2: add new option to file root notes.\nv0.14.1: rebuild.\nv0.14.0: new '/Generate repeats' command.\nv0.13.0: '/List conflicted notes' now clears out all copies, and offers side-by-side viewing of conflicted note versions. Also bug fixes.\nv0.12.1: '/List conflicted notes' now covers Calendar notes as well.\nv0.12.0: add more capability to '/List conflicted notes'.\nv0.11.0: new command '/find doubled notes'.\nv0.10.0: fix bug in moving top level tasks, and adds support for indented tasks.",
1111
"plugin.dependencies": [],
1212
"plugin.script": "script.js",
1313
"plugin.url": "https://github.com/NotePlan/plugins/blob/main/np.Tidy/README.md",

np.Tidy/src/index.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,26 @@
22
//-----------------------------------------------------------------------------
33
// Tidy plugin
44
// Jonathan Clark
5-
// Last updated 2025-02-16 for v0.14.7 by @jgclark
5+
// Last updated 2025-02-16 for v0.14.8 by @jgclark
66
//-----------------------------------------------------------------------------
77

88
// allow changes in plugin.json to trigger recompilation
99
import pluginJson from '../plugin.json'
1010
import { JSP, logDebug, logError, logInfo } from '@helpers/dev'
1111
import { pluginUpdated, updateSettingData } from '@helpers/NPConfiguration'
1212
import { editSettings } from '@helpers/NPSettings'
13+
import { findHeadingInNotes } from '@helpers/NPParagraph'
1314

1415
const pluginID = 'np.Tidy'
1516

17+
export {
18+
removeSectionFromAllNotes,
19+
removeSectionFromRecentNotes,
20+
} from './removeSections'
1621
export {
1722
logNotesChangedInInterval,
1823
removeDoneMarkers,
1924
removeOrphanedBlockIDs,
20-
removeSectionFromAllNotes,
21-
removeSectionFromRecentNotes,
2225
removeTriggersFromRecentCalendarNotes,
2326
removeDoneTimeParts,
2427
removeBlankNotes,
@@ -37,7 +40,7 @@ export { listPotentialDoubles } from './doubledNotes'
3740
* Other imports/exports
3841
*/
3942
// eslint-disable-next-line import/order
40-
export { onUpdateOrInstall, init, onSettingsUpdated } from './triggers-hooks'
43+
export { onUpdateOrInstall, init, onSettingsUpdated } from './triggersHooks'
4144

4245
// Note: not yet written or used:
4346
// export { onOpen, onEditorWillSave } from './NPTriggers-Hooks'
@@ -47,7 +50,7 @@ export { onUpdateOrInstall, init, onSettingsUpdated } from './triggers-hooks'
4750
* Plugin entrypoint for command: "/<plugin>: Update Plugin Settings/Preferences"
4851
* @author @dwertheimer
4952
*/
50-
export async function updateSettings() {
53+
export async function updateSettings(): Promise<void> {
5154
try {
5255
logDebug(pluginJson, `updateSettings running`)
5356
await editSettings(pluginJson)

np.Tidy/src/removeSections.js

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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

Comments
 (0)