Skip to content

Commit 4d5cfad

Browse files
committed
Merge branch 'main' of https://github.com/NotePlan/plugins
2 parents 12a0fbc + 8b3f490 commit 4d5cfad

11 files changed

Lines changed: 188 additions & 127 deletions

File tree

helpers/NPParagraph.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1831,9 +1831,8 @@ export function removeAllDueDates(filename: string): boolean {
18311831
}
18321832

18331833
/**
1834-
* WARNING: This was an attempt to work around the issue that .lineIndex in Editor.paragraphs includes frontmatter lines, but in Editor.selectedParagraphs it doesn't.
1835-
* WARNING: However, it doesn't work, as it throws up "Attempted to assign to readonly property" errors.
18361834
* Get the selected paragraphs with the correct line index, taking into account the frontmatter lines.
1835+
* Note: This attempts to work around the issue that .lineIndex in Editor.paragraphs includes frontmatter lines, but in Editor.selectedParagraphs it doesn't.
18371836
* Note: should really live in editor.js, but putting here to avoid a circular dependency.
18381837
* @author @jgclark
18391838
*
@@ -1846,11 +1845,13 @@ export function getSelectedParagraphsWithCorrectLineIndex(): Array<TParagraph> {
18461845
return []
18471846
}
18481847
const numberOfFrontmatterLines = endOfFrontmatterLineIndex(note) || 0
1848+
logDebug('getSelectedParagraphsWithCorrectLineIndex', `numberOfFrontmatterLines: ${String(numberOfFrontmatterLines)}`)
18491849
const selectedParagraphs = Editor.selectedParagraphs.map((p) => Editor.paragraphs[p.lineIndex]) ?? []
1850+
logDebug('getSelectedParagraphsWithCorrectLineIndex', `Editor.selectedParagraphs:\n${String(Editor.selectedParagraphs.map((p) => `- ${p.lineIndex}: ${p.content}`).join('\n'))}`)
18501851
const correctedSelectedParagraphs: Array<TParagraph> = selectedParagraphs.slice()
18511852
correctedSelectedParagraphs.forEach((p) => {
18521853
// $FlowIgnore[cannot-write]
1853-
p.lineIndex += numberOfFrontmatterLines
1854+
p.lineIndex += numberOfFrontmatterLines + 1
18541855
})
18551856

18561857
logDebug('getSelectedParagraphsWithCorrectLineIndex', `${correctedSelectedParagraphs.length} Corrected selected paragraph(s) with lineIndex taking into account frontmatter lines:\n${String(correctedSelectedParagraphs.map((p) => `- ${p.lineIndex}: ${p.content}`).join('\n'))}`)

helpers/NPVersions.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export function usersVersionHas(feature: string): boolean {
3131
getWeather: '3.19.2', // Nov 2025
3232
mainSidebarControl: '3.19.2', // Nov 2025
3333
contentDeduplicator: '3.19.2', // Nov 2025
34+
settableLineIndex: '3.19.2', // Nov 2025, build 1440
3435
}
3536

3637
// Check if the user's version meets the requirement for the requested feature

helpers/NPnote.js

Lines changed: 74 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -185,18 +185,36 @@ export async function chooseNoteV2(
185185

186186
// Now show the options to the user
187187
let noteToReturn = null
188+
let selectedNote: ?TNote
189+
let selectedText: string
188190
if (usersVersionHas('decoratedCommandBar')) {
189191
// logDebug('chooseNoteV2', `Using 3.18.0's advanced options for CommandBar.showOptions call`)
190192
// use the more advanced options to the `CommandBar.showOptions` call
191193
const { index } = await CommandBar.showOptions(opts, promptText)
192-
noteToReturn = opts[index].text.includes('[New note]') ? await getOrMakeCalendarNote(sortedNoteList[index].title ?? '') : sortedNoteList[index]
194+
selectedNote = sortedNoteList[index]
195+
selectedText = opts[index].text
193196
} else {
194197
// use the basic options for the `CommandBar.showOptions` call. Get this by producing a simple array from the main options array.
195198
// logDebug('chooseNoteV2', `Using pre-3.18.0's basic options for CommandBar.showOptions call`)
196199
const simpleOpts = opts.map((opt) => opt.text)
197200
const { index } = await CommandBar.showOptions(simpleOpts, promptText)
198-
noteToReturn = simpleOpts[index].includes('[New note]') ? await getOrMakeCalendarNote(sortedNoteList[index].title ?? '') : sortedNoteList[index]
201+
selectedNote = sortedNoteList[index]
202+
selectedText = simpleOpts[index]
199203
}
204+
if (selectedText.includes('[New note]')) {
205+
// Handle "[New note]" option - create a new regular note
206+
// Prompt user for note title
207+
const noteTitle = await CommandBar.showInput('Enter title for new note:', 'New note')
208+
if (noteTitle && noteTitle !== '') {
209+
const newNoteFilename = await DataStore.newNote(noteTitle, '/')
210+
if (newNoteFilename) {
211+
noteToReturn = await DataStore.noteByFilename(newNoteFilename, 'Notes')
212+
}
213+
}
214+
} else {
215+
noteToReturn = selectedNote
216+
}
217+
200218
// logDebug('chooseNoteV2', `-> ${noteToReturn ? noteToReturn.filename : '(none)'}`)
201219
return noteToReturn
202220
}
@@ -1158,62 +1176,75 @@ export function findNotesMatchingHashtagOrMentionFromList(
11581176
*
11591177
* @param {TNote} note - note to get headings from
11601178
* @param {boolean} includeMarkdown - whether to include markdown markers in the headings
1161-
* @param {boolean} optionAddATopAndtBottom - whether to add 'top of note' and 'bottom of note' options. Default: true.
1179+
* @param {boolean} optionAddATopAndBottom - whether to add 'top of note' and 'bottom of note' options. Default: true.
11621180
* @param {boolean} optionCreateNewHeading - whether to offer to create a new heading at top or bottom of note. Default: false.
11631181
* @param {boolean} includeArchive - whether to include headings in the Archive section of the note (i.e. after 'Done'). Default: false.
11641182
* @return {Array<string>}
11651183
*/
11661184
export function getHeadingsFromNote(
11671185
note: TNote,
11681186
includeMarkdown: boolean = false,
1169-
optionAddATopAndtBottom: boolean = true,
1187+
optionAddATopAndBottom: boolean = true,
11701188
optionCreateNewHeading: boolean = false,
11711189
includeArchive: boolean = false,
11721190
): Array<string> {
1173-
let headingStrings = []
1174-
const spacer = '#'
1175-
let headingParas: Array<TParagraph> = []
1176-
const indexEndOfActive = findEndOfActivePartOfNote(note)
1177-
if (includeArchive) {
1178-
headingParas = note.paragraphs.filter((p) => p.type === 'title' && p.lineIndex < indexEndOfActive)
1179-
} else {
1180-
headingParas = note.paragraphs.filter((p) => p.type === 'title')
1181-
}
1191+
try {
1192+
const indexEndOfActive = findEndOfActivePartOfNote(note)
1193+
const MDHeadingChar = '#'
1194+
let headingStrings = []
1195+
let headingParas: Array<TParagraph> = []
1196+
if (includeArchive) {
1197+
headingParas = note.paragraphs.filter((p) => p.type === 'title' && p.lineIndex <= indexEndOfActive)
1198+
} else {
1199+
headingParas = note.paragraphs.filter((p) => p.type === 'title')
1200+
}
11821201

1183-
// If this is the title line, skip it
1184-
if (headingParas.length > 0) {
1185-
if (headingParas[0].content === note.title) {
1186-
headingParas = headingParas.slice(1)
1202+
// If this is the title line, skip it
1203+
if (headingParas.length > 0) {
1204+
if (headingParas[0].content === note.title) {
1205+
headingParas = headingParas.slice(1)
1206+
}
11871207
}
1188-
}
1189-
if (headingParas.length > 0) {
1190-
headingStrings = headingParas.map((p) => {
1191-
let prefix = ''
1192-
for (let i = 0; i < p.headingLevel; i++) {
1193-
prefix += spacer
1208+
if (headingParas.length > 0) {
1209+
headingStrings = headingParas.map((p) => {
1210+
let prefix = ''
1211+
for (let i = 0; i < p.headingLevel; i++) {
1212+
prefix += MDHeadingChar
1213+
}
1214+
return `${prefix} ${p.content.trimLeft()}`
1215+
})
1216+
}
1217+
if (optionCreateNewHeading) {
1218+
// Note: The strings here must match the strings in userInput.js::processChosenHeading()
1219+
if (note.type === 'Calendar') {
1220+
headingStrings.unshift('➕#️⃣ (first insert new heading at the start of the note)')
1221+
} else {
1222+
headingStrings.unshift(`➕#️⃣ (first insert new heading under the title)`)
1223+
}
1224+
if (headingParas.length > 0) {
1225+
headingStrings.push(`➕#️⃣ (first insert new heading at the end of the note)`)
1226+
} else {
1227+
logDebug('NPnote/getHeadingsFromNote', `No headings found in note ${note.filename}. So not adding 'insert new heading at the end of the note' option as well as 'first insert new heading...' option.`)
11941228
}
1195-
return `${prefix} ${p.content.trimLeft()}`
1196-
})
1197-
}
1198-
if (optionCreateNewHeading) {
1199-
if (note.type === 'Calendar') {
1200-
headingStrings.unshift('➕#️⃣ (first insert new heading at the start of the note)')
1201-
} else {
1202-
headingStrings.unshift(`➕#️⃣ (first insert new heading under the title)`)
12031229
}
1204-
headingStrings.push(`➕#️⃣ (first insert new heading at the end of the note)`)
1205-
}
1206-
if (optionAddATopAndtBottom) {
1207-
headingStrings.unshift(' (top of note)')
1208-
headingStrings.push(' (bottom of note)')
1209-
}
1210-
if (headingStrings.length === 0) {
1211-
return ['']
1212-
}
1213-
if (!includeMarkdown) {
1214-
headingStrings = headingStrings.map((h) => h.replace(/^#{1,5}\s*/, '')) // remove any markdown heading markers
1230+
logDebug('NPnote/getHeadingsFromNote', `After adding 'insert new heading...' options, headingStrings: ${String(headingStrings)}`)
1231+
if (optionAddATopAndBottom) {
1232+
headingStrings.unshift('⏫ (top of note)')
1233+
headingStrings.push('⏬ (bottom of note)')
1234+
}
1235+
if (headingStrings.length === 0) {
1236+
logDebug('NPnote/getHeadingsFromNote', `No headingStrings generated for note ${note.filename}. Returning empty array.`)
1237+
return []
1238+
}
1239+
// Remove any markdown heading markers if requested
1240+
if (!includeMarkdown) {
1241+
headingStrings = headingStrings.map((h) => h.replace(/^#{1,5}\s*/, ''))
1242+
}
1243+
return headingStrings
1244+
} catch (error) {
1245+
logError('NPnote/getHeadingsFromNote', error.message)
1246+
return []
12151247
}
1216-
return headingStrings
12171248
}
12181249

12191250
/**

helpers/paragraph.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ export function findStartOfActivePartOfNote(note: CoreNoteFields, allowPreamble?
583583
// or 'allowPreamble' is true.
584584
// If there is, run on to next heading or blank line (if found) otherwise, just the next line. Finding a separator or any YouTutype of task or checklist also stops the search.
585585
if (allowPreamble || (paras[startOfActive] && paras[startOfActive].type === 'text' && paras[startOfActive].content.match(/^#\w/))) {
586-
// logDebug('paragraph/findStartOfActivePartOfNote', `- We want to allow preamble, or found a metadata line.`)
586+
// logDebug('paragraph/findStartOfActivePartOfNote', `- We want to allow preamble, or there's a hashtag starting the next line.`)
587587
// startOfActive += 1
588588
for (let i = startOfActive; i < paras.length; i++) {
589589
const p = paras[i]

helpers/userInput.js

Lines changed: 79 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,7 @@ export async function chooseHeading(
741741
* Ask user to select a heading from those in a given note (regular or calendar), or optionally create a new heading at top or bottom of note to use, or the top or bottom of the note.
742742
* Note: Any whitespace on the end of the heading text is left in place, as otherwise this would cause issues with NP API calls that take heading parameter.
743743
* V2: Can use newer CommandBar decorated options, if running on v3.18 or later.
744+
* Note: Returns an empty string if no headings found and no other options requested.
744745
* @author @jgclark
745746
*
746747
* @param {TNote} note - note to draw headings from
@@ -763,10 +764,17 @@ export async function chooseHeadingV2(
763764
return await chooseHeading(note, optionAddAtTopAndBottom, optionCreateNewHeading, includeArchive, headingLevel)
764765
}
765766

766-
// Get the existing headings from the note, with markdown markers, but without any 'create new heading' options
767-
const headingStrings = getHeadingsFromNote(note, true, false, false, includeArchive)
767+
// Get the existing headings from the note, with markdown markers, but without any 'create new heading' options (as they get added later)
768+
const headings = getHeadingsFromNote(note, true, false, false, includeArchive)
769+
770+
// If there are no headings, and the other options are false, then return an empty string
771+
if (headings.length === 0 && !optionAddAtTopAndBottom && !optionCreateNewHeading) {
772+
logDebug('userInput / chooseHeadingV2', `No headings found in note ${note.filename}, and no other options requested. So returning an empty string.`)
773+
return ''
774+
}
775+
768776
const headingOptions: Array<TCommandBarOptionObject> = []
769-
headingStrings.forEach((heading) => {
777+
headings.forEach((heading) => {
770778
const headingWithoutLeadingMarkers = heading.replace(/^#{1,5}\s*/, '')
771779
const headingLevel = heading.match(/^#{1,5}/)?.[0]?.length ?? 2
772780
headingOptions.push({
@@ -787,14 +795,16 @@ export async function chooseHeadingV2(
787795
alpha: 0.6,
788796
darkAlpha: 0.6,
789797
})
790-
headingOptions.push({
791-
text: '(insert new heading at the end of the note)',
792-
icon: 'h' + String(headingLevel),
793-
shortDescription: 'Add new',
794-
color: 'orange-500',
795-
alpha: 0.6,
796-
darkAlpha: 0.6,
797-
})
798+
if (headings.length > 0) {
799+
headingOptions.push({
800+
text: '(insert new heading at the end of the note)',
801+
icon: 'h' + String(headingLevel),
802+
shortDescription: 'Add new',
803+
color: 'orange-500',
804+
alpha: 0.6,
805+
darkAlpha: 0.6,
806+
})
807+
}
798808
}
799809
if (optionAddAtTopAndBottom) {
800810
headingOptions.unshift({
@@ -835,64 +845,72 @@ export async function chooseHeadingV2(
835845
* @param {TNote} note
836846
* @param {string} chosenHeading - The text of the new heading to add, or 5 possible special instruction strings.
837847
* @param {number?} headingLevel - The level of the heading to add (1-5) where requested. If not given, will default to 2.
838-
* @returns {string} headingToReturn - The heading to return, or one of the special instruction strings <<top of note>>, <<bottom of note>>.
848+
* @returns {string} headingToReturn - The heading to return, or one of the special instruction strings <<top of note>>, <<bottom of note>>. Or empty string if user cancelled operation.
839849
*/
840850
export async function processChosenHeading(note: TNote, chosenHeading: string, headingLevel: number = 2): Promise<string> {
841-
if (chosenHeading === '') {
842-
throw new Error('No heading passed to processChosenHeading(). Stopping.')
843-
}
851+
try {
852+
if (chosenHeading === '') {
853+
throw new Error('No heading passed to processChosenHeading(). Stopping.')
854+
}
844855

845-
let newHeading: string | boolean
846-
let headingToReturn = chosenHeading
847-
logDebug('userInput / processChosenHeading', `headingLevel: ${headingLevel} chosenHeading: '${chosenHeading}'`)
856+
let newHeading: string | boolean
857+
let headingToReturn = chosenHeading
858+
logDebug('userInput / processChosenHeading', `headingLevel: ${headingLevel} chosenHeading: '${chosenHeading}'`)
848859

849-
if (headingToReturn.includes('insert new heading at the start of the note')) {
860+
if (headingToReturn.includes('insert new heading at the start of the note')) {
850861
// ask for new heading, and insert right at top
851-
newHeading = await getInput(`Enter heading to add at the start of the note`)
852-
if (newHeading && typeof newHeading === 'string') {
853-
const startPos = 0
854-
// $FlowIgnore
855-
note.insertHeading(newHeading, startPos, headingLevel)
856-
logDebug('userInput / processChosenHeading', `prepended new heading '${newHeading}' at line ${startPos} (calendar note)`)
857-
headingToReturn = newHeading
858-
} else {
859-
throw new Error(`user cancelled operation`)
860-
}
861-
} else if (headingToReturn.includes('insert new heading under the title')) {
862-
// ask for new heading, find smart insertion position, and insert it
863-
newHeading = await getInput(`Enter heading to add at the start of the note`)
864-
if (newHeading && typeof newHeading === 'string') {
865-
const startPos = findStartOfActivePartOfNote(note)
866-
// $FlowIgnore
867-
note.insertHeading(newHeading, startPos, headingLevel)
868-
logDebug('userInput / processChosenHeading', `prepended new heading '${newHeading}' at line ${startPos} (project note)`)
869-
headingToReturn = newHeading
870-
} else {
871-
throw new Error(`user cancelled operation`)
872-
}
873-
} else if (headingToReturn.includes('insert new heading at the end of the note')) {
874-
// ask for new heading, and then append it
875-
newHeading = await getInput(`Enter heading to add at the end of the note`)
876-
if (newHeading && typeof newHeading === 'string') {
877-
const indexEndOfActive = findEndOfActivePartOfNote(note)
878-
const newLindeIndex = indexEndOfActive + 1
879-
// $FlowIgnore - headingLevel is a union type, and we've already checked it's a number
880-
note.insertHeading(newHeading, newLindeIndex, headingLevel || 2)
881-
logDebug('userInput / processChosenHeading', `appended new heading '${newHeading}' at line ${newLindeIndex}`)
882-
headingToReturn = newHeading
862+
newHeading = await getInput(`Enter heading to add at the start of the note`, 'OK', 'New Heading')
863+
if (newHeading && typeof newHeading === 'string') {
864+
const startPos = 0
865+
// $FlowIgnore
866+
note.insertHeading(newHeading, startPos, headingLevel)
867+
logDebug('userInput / processChosenHeading', `prepended new heading '${newHeading}' at line ${startPos} (calendar note)`)
868+
headingToReturn = newHeading
869+
} else {
870+
logWarn('userInput / processChosenHeading', `User cancelled operation`)
871+
return ''
872+
}
873+
} else if (headingToReturn.includes('insert new heading under the title')) {
874+
// ask for new heading, find smart insertion position, and insert it
875+
newHeading = await getInput(`Enter heading to add under the title`, 'OK', 'New Heading')
876+
if (newHeading && typeof newHeading === 'string') {
877+
const startPos = findStartOfActivePartOfNote(note)
878+
// $FlowIgnore
879+
note.insertHeading(newHeading, startPos, headingLevel)
880+
logDebug('userInput / processChosenHeading', `prepended new heading '${newHeading}' at line ${startPos} (project note)`)
881+
headingToReturn = newHeading
882+
} else {
883+
logWarn('userInput / processChosenHeading', `User cancelled operation`)
884+
return ''
885+
}
886+
} else if (headingToReturn.includes('insert new heading at the end of the note')) {
887+
// ask for new heading, and then append it
888+
newHeading = await getInput(`Enter heading to add at the end of the note`, 'OK', 'New Heading')
889+
if (newHeading && typeof newHeading === 'string') {
890+
const indexEndOfActive = findEndOfActivePartOfNote(note)
891+
const newLindeIndex = indexEndOfActive + 1
892+
// $FlowIgnore - headingLevel is a union type, and we've already checked it's a number
893+
note.insertHeading(newHeading, newLindeIndex, headingLevel || 2)
894+
logDebug('userInput / processChosenHeading', `appended new heading '${newHeading}' at line ${newLindeIndex}`)
895+
headingToReturn = newHeading
896+
} else {
897+
logWarn('userInput / processChosenHeading', `User cancelled operation`)
898+
return ''
899+
}
900+
} else if (headingToReturn.includes('(top of note)')) {
901+
logDebug('userInput / processChosenHeading', `selected top of note, rather than a heading`)
902+
headingToReturn = '<<top of note>>' // hopefully won't ever be used as an actual title!
903+
} else if (headingToReturn.includes('(bottom of note)')) {
904+
logDebug('userInput / processChosenHeading', `selected end of note, rather than a heading`)
905+
headingToReturn = '<<bottom of note>>'
883906
} else {
884-
throw new Error(`user cancelled operation`)
907+
// Nothing else to do
885908
}
886-
} else if (headingToReturn.includes('(top of note)')) {
887-
logDebug('userInput / processChosenHeading', `selected top of note, rather than a heading`)
888-
headingToReturn = '<<top of note>>' // hopefully won't ever be used as an actual title!
889-
} else if (headingToReturn.includes('(bottom of note)')) {
890-
logDebug('userInput / processChosenHeading', `selected end of note, rather than a heading`)
891-
headingToReturn = '<<bottom of note>>'
892-
} else {
893-
// Nothing else to do
909+
return headingToReturn
910+
} catch (error) {
911+
logError('userInput / processChosenHeading', error.message)
912+
return ''
894913
}
895-
return headingToReturn
896914
}
897915

898916
/**

jgclark.Filer/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# What's changed in 📦 Filer plugin?
22
Please see the [Readme for this plugin](https://github.com/NotePlan/plugins/tree/main/jgclark.Filer) for more details, including the available settings.
33

4-
## [1.3.3] - 2025-10-18
4+
## [1.3.3] - 2025-11-07
55
- fix duplication in /move block (thanks for the tip, @bido_1977)
6+
- improved the heading picker used in some of these commands
67

78
## [1.3.2] - 2025-09-06
89
- suppress notes in the special Archive and Template folders from the note chooser in **/add sync'd copy to note** and **/move ...** commands (for @chrismetcalf)

0 commit comments

Comments
 (0)