Skip to content

Commit e2c13aa

Browse files
jgclarkcursoragent
andcommitted
Merge projects-new-layout into main
Co-authored-by: Cursor <cursoragent@cursor.com>
2 parents 179b477 + 1f83bfc commit e2c13aa

74 files changed

Lines changed: 7474 additions & 2375 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎__mocks__/Note.mock.js‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { textWithoutSyncedCopyTag } from '@helpers/syncedCopies'
1313
export class Note {
1414
// Explicitly define properties that are dynamically assigned
1515
content: string
16+
/** Full note markdown when tests construct notes with `{ rawContent }` only */
17+
rawContent: string = ''
1618
// Properties
1719
backlinks: any[] = [] /* sample: [ SOMETHING ], */
1820
changedDate: any = {} /* new Date("Tue Sep 07 2021 06:49:41 GMT-0700 (PDT)"), */
@@ -242,6 +244,12 @@ export class Note {
242244
this._content = value
243245
this.paragraphs = makeParagraphsFromContent(this._content)
244246
}
247+
} else if (key === 'rawContent') {
248+
this.rawContent = value
249+
if (typeof value === 'string' && value !== '' && (data.content === undefined || data.content === '')) {
250+
this._content = value
251+
this.paragraphs = makeParagraphsFromContent(this._content)
252+
}
245253
} else {
246254
this[key] = data[key]
247255
}

‎helpers/HTMLView.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import showdown from 'showdown' // for Markdown -> HTML from https://github.com/
88
import { hasFrontMatter } from '@helpers/NPFrontMatter'
99
import { getFolderFromFilename } from '@helpers/folders'
1010
import { clo, logDebug, logError, logInfo, logWarn, JSP, timer } from '@helpers/dev'
11-
import { getStoredWindowRect, getWindowFromCustomId, isHTMLWindowOpen, storeWindowRect } from '@helpers/NPWindows'
11+
import { getStoredWindowRect, getWindowFromCustomId, getWindowIdFromCustomId, isHTMLWindowOpen, storeWindowRect } from '@helpers/NPWindows'
1212
import { generateCSSFromTheme, RGBColourConvert } from '@helpers/NPThemeToCSS'
1313
import { isTermInEventLinkHiddenPart, isTermInNotelinkOrURI, isTermInMarkdownPath } from '@helpers/paragraph'
1414
import { RE_EVENT_LINK, RE_SYNC_MARKER, formRegExForUsersOpenTasks } from '@helpers/regex'
@@ -146,6 +146,7 @@ export async function getNoteContentAsHTML(content: string, note: TNote): Promis
146146
// Make some necessary changes before conversion to HTML
147147
for (let i = 0; i < lines.length; i++) {
148148
// remove any sync link markers (blockIds)
149+
// TODO: there's a helper function for this, I think.
149150
lines[i] = lines[i].replace(/\^[A-z0-9]{6}([^A-z0-9]|$)/g, '').trimRight()
150151

151152
// change open tasks to GFM-flavoured task syntax
@@ -716,6 +717,7 @@ export async function sendToHTMLWindow(windowId: string, actionType: string, dat
716717

717718
const windowExists = isHTMLWindowOpen(windowId)
718719
if (!windowExists) logWarn(`sendToHTMLWindow`, `Window ${windowId} does not exist; setting NPWindowID = undefined`)
720+
// TEST: Not sure the comment about iphone/ipad is still relevant, but leaving it in for now.
719721
const windowIdToSend = windowExists ? windowId : undefined // for iphone/ipad you have to send undefined
720722

721723
const dataWithUpdated = {

‎helpers/NPConfiguration.js‎

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,55 +55,79 @@ export async function initConfiguration(pluginJsonData: any): Promise<any> {
5555
return migrateData
5656
}
5757

58+
/**
59+
* Return the Default for a setting from plugin.json.
60+
* Only return empty string when default is null/undefined, so false and 0 are preserved.
61+
* @param {any} setting - single entry from plugin.settings
62+
* @returns {any}
63+
*/
64+
function getDefaultValueForNewSetting(setting: any): any {
65+
const d = setting?.default
66+
return d === undefined || d === null ? '' : d
67+
}
68+
5869
/**
5970
* Add new top-level keys to setting.json's data in the event plugin.settings object has been updated in a Plugin's plugin.json file.
6071
* @author @codedungeon
6172
* @param {any} pluginJsonData - plugin.json data for which plugin is being migrated
62-
* @return {number} update result (1 settings updated, 0 no update necessary, -1 update failed)
73+
* @return {number} update result (1 settings updated, 0 no update necessary, -1 update failed or invalid pluginJsonData)
6374
*/
6475
export function updateSettingData(pluginJsonData: any): number {
6576
let updateResult = 0
6677

78+
if (pluginJsonData == null || typeof pluginJsonData !== 'object' || Array.isArray(pluginJsonData)) {
79+
logWarn(
80+
'NPConfiguration/updateSettingData',
81+
'Invalid pluginJsonData: expected a non-null object (not an array). Skipping settings migration.',
82+
)
83+
return -1
84+
}
85+
6786
const newSettings = {}
6887
const currentSettingData = DataStore.settings
6988

7089
const pluginSettings = pluginJsonData.hasOwnProperty('plugin.settings') ? pluginJsonData['plugin.settings'] : []
90+
// clo(pluginSettings, `pluginSettings`)
7191
pluginSettings.forEach((setting) => {
72-
const key: any = setting?.key || null
73-
if (key) {
92+
const key: any = setting?.key
93+
const hasValidKey = key != null && key !== ''
94+
// For each setting with a key (i.e. ignoring headings) check if it is in the current settings, and if not, add it with the default value.
95+
if (hasValidKey) {
7496
if (!currentSettingData.hasOwnProperty(key)) {
75-
newSettings[key] = setting?.default || ''
97+
newSettings[key] = getDefaultValueForNewSetting(setting)
98+
logInfo('updateSettingData', `- Added new setting: ${key} = ${newSettings[key]}`)
7699
updateResult = 1 // we have made at least one update, change result code accordingly
77100
} else {
78101
newSettings[key] = currentSettingData[key]
79102
}
80103
}
81104
})
105+
82106
// logDebug(`NPConfiguration/updateSettingData: Object.keys(DataStore): ${Object.keys(DataStore).join(',')}`)
83107
// logDebug('currentSettingData:', JSP(currentSettingData, 2))
84108
// logDebug('newSettings:', JSP(newSettings, 2))
85109
// logDebug('DataStore.settings:', JSP(DataStore.settings, 2))
86110
try {
87-
if (DataStore && typeof DataStore.settings === 'object' && updateResult > 0) {
111+
if (updateResult > 0 && DataStore && typeof DataStore.settings === 'object') {
88112
// WARNING: @jgclark at least once saw an 'undefined is not an object' error, which appeared to be for this line.
89113
// dbw added the following logging to try to track it down but it looks like, JS thinks that DataStore is not an object at times.
90114
// And yet, somehow the migration actually does work and migrates new settings. So, I'm not sure what's going on here.
91115
// We are going to leave this alone for the time being, but if you see this error again, please uncomment the following to keep hunting.
92-
logDebug(
93-
`NPConfiguration/updateSettingData for ${pluginJsonData['plugin.id']} updateResult: ${updateResult}`,
94-
`typeof DataStore: ${typeof DataStore} isArray:${String(
95-
Array.isArray(DataStore),
96-
)} typeof DataStore.settings: ${typeof DataStore?.settings} typeof newSettings: ${typeof newSettings}`,
97-
)
116+
// logDebug(
117+
// `NPConfiguration/updateSettingData for ${pluginJsonData['plugin.id']} updateResult: ${updateResult}`,
118+
// `typeof DataStore: ${typeof DataStore} isArray:${String(
119+
// Array.isArray(DataStore),
120+
// )} typeof DataStore.settings: ${typeof DataStore?.settings} typeof newSettings: ${typeof newSettings}`,
121+
// )
98122
logDebug(
99123
'NPConfiguration/updateSettingData',
100124
`About to update DataStore.settings to newSettings after an update. If you see a TypeError right after this, please ignore it. It's a known NP bug that doesn't seem to matter.`,
101125
)
102126
DataStore.settings = newSettings
103127
}
104128
} catch (error) {
105-
console.log(
106-
'NPConfiguration/updateSettingData/Plugin Settings Migration Failed. Was not able to automatically migrate your plugin settings to the new version. Please open the plugin settings and save in order to update your settings.',
129+
logError('updateSettingData',
130+
'Plugin Settings Migration Failed. Was not able to automatically migrate your plugin settings to the new version. Please open the plugin settings, check them, and then save in order to update your settings.',
107131
)
108132
updateResult = -1
109133
}
@@ -568,6 +592,7 @@ export async function getSettingFromAnotherPlugin(pluginID: string, settingName:
568592

569593
/**
570594
* Backup the settings.json file for 'pluginID' to a dated version in the plugin data folder.
595+
* Note: this fails if the file is not valid JSON, unfortunately. @jgclark can't find a way around this.
571596
* @author @jgclark
572597
* @param {string} pluginID
573598
* @param {string} reason
@@ -579,12 +604,15 @@ export async function backupSettings(pluginID: string, reason: string = 'backup'
579604
const pluginSettings = await DataStore.loadJSON(`../${pluginID}/settings.json`)
580605
const backupFilename = `settings_${reason}_${moment().format('YYYYMMDDHHmmss')}.json`
581606
const backupPath = `../${pluginID}/${backupFilename}`
582-
await DataStore.saveJSON(pluginSettings, backupPath)
607+
const res = await DataStore.saveJSON(pluginSettings, backupPath)
608+
if (!res) {
609+
throw new Error(`Error saving backup to ${backupPath}`)
610+
}
583611
if (!suppressMessage) {
584612
await showMessage(`Backup of ${pluginID} settings saved to ${backupPath}`, 'OK', `${pluginID} Settings Backup`)
585613
}
586614
logInfo('backupSettings', `Backup of ${pluginID} settings saved to ${backupPath}`)
587-
return true
615+
return res
588616
} catch (error) {
589617
if (!suppressMessage) {
590618
await showMessage(`Error trying to Backup ${pluginID} settings. Please see Plugin Console log for details.`, 'OK', `${pluginID} Settings Backup`)

‎helpers/NPEditor.js‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,62 @@ export function isNoteOpenInEditor(filename: string): boolean {
239239
return false
240240
}
241241

242+
/**
243+
* Returns the first open Editor window that matches a given filename (if any).
244+
* If 'getLastOpenEditor' is true, then return the last matching open Editor window (which is the most recently opened one) instead.
245+
* TEST: Changes 30.3.2026
246+
* @author @jgclark
247+
* @param {string} openNoteFilename to find in list of open Editor windows
248+
* @param {boolean} getLastOpenEditor - whether to return the last open Editor window (which is the most recently opened one) instead of the first one that matches the filename (the default)
249+
* @returns {TEditor | false} the matching open Editor window or false if not found
250+
*/
251+
export function getOpenEditorFromFilename(openNoteFilename: string, getLastOpenEditor: boolean = false): TEditor | false {
252+
try {
253+
const allEditorWindows = NotePlan.editors
254+
const matchingEditorWindows = allEditorWindows?.filter(ew => ew.filename === openNoteFilename) ?? []
255+
if (matchingEditorWindows.length === 0) {
256+
logDebug('getOpenEditorFromFilename', `No open Editor window found for filename '${openNoteFilename}'`)
257+
return false
258+
}
259+
if (getLastOpenEditor) {
260+
return matchingEditorWindows[matchingEditorWindows.length - 1]
261+
}
262+
return matchingEditorWindows[0]
263+
} catch (error) {
264+
logError('getOpenEditorFromFilename', error.message)
265+
return false
266+
}
267+
}
268+
269+
/**
270+
* Find a regular (folder) note that is open in some Editor pane — not only the focused one.
271+
* Prefers the globally focused `Editor` when its note is already type 'Notes'; otherwise scans
272+
* `NotePlan.editors` so split views with a calendar note focused still expose an open project note.
273+
* @author @jgclark
274+
* @returns {?TNote} the note, or null if no Editor pane shows a regular note
275+
*/
276+
export function getFirstRegularNoteAmongOpenEditors(): ?TNote {
277+
try {
278+
const focusedNote = Editor?.note
279+
if (focusedNote && focusedNote.type === 'Notes') {
280+
return focusedNote
281+
}
282+
const allEditorWindows = NotePlan.editors ?? []
283+
for (const thisEditorWindow of allEditorWindows) {
284+
const candidate = thisEditorWindow?.note
285+
if (candidate && candidate.type === 'Notes') {
286+
logDebug('getFirstRegularNoteAmongOpenEditors', `Using open editor pane for '${candidate.filename || candidate.title || '?'}' (focused pane was not a regular note)`)
287+
return candidate
288+
}
289+
}
290+
logDebug('getFirstRegularNoteAmongOpenEditors', `No open Editor pane contains a regular (Notes) note`)
291+
return null
292+
} catch (error) {
293+
logError('getFirstRegularNoteAmongOpenEditors', error.message)
294+
return null
295+
}
296+
}
297+
242298
/**
243299
* Show an existing note in an Editor window, identified by its filename.
244300
* Uses smart features to determine which window or split view to open the note in:

0 commit comments

Comments
 (0)