diff --git a/CHANGELOG.md b/CHANGELOG.md index 814d29c..7282070 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `Open Preview` in the right-click menu of a Markdown note, like in the Explorer (#10) * `Notes: List Notes` (`Alt+L`) shows the notes you opened recently first, like Quick Open, wherever you opened them from, and `Notes: Clear Recently Opened` forgets them (#18) +* a `notes.newNoteName` setting to fill in the name of a new note from a template with VS Code's snippet date and time variables, like `${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}`, so `Enter` creates it and typing replaces it (#61) * a Sort By button after Search in each section, to sort notes and folders by name (A to Z or Z to A) or newest first by date modified or created, saved in the new `notes.sortOrder` setting; folders stay first, and List Notes uses the same order after the recently opened notes (#22, #63) ### Changed diff --git a/README.md b/README.md index 673c6d4..5342c31 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ This extension contributes the following settings: * `notes.notesLocation`: location of the notes in the Notes section, set in your user settings and not synced between machines. In a [portable](https://code.visualstudio.com/docs/editor/portable) VS Code, a relative path is relative to the folder holding VS Code, so the notes can travel with it (for example `data/Notes` on Windows and Linux) * `notes.workspaceNotesLocation`: location of the notes in the Workspace Notes section, relative to the workspace folder or a full path * `notes.notesDefaultNoteExtension`: extension used for new notes that don't have one in their name +* `notes.newNoteName`: name filled in when you create a note, using VS Code's [snippet date and time variables](https://code.visualstudio.com/docs/editing/userdefinedsnippets#_variables), for example `${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} ${CURRENT_HOUR}-${CURRENT_MINUTE}`; press `Enter` to use it or type a name instead * `notes.sortOrder`: how notes and folders are sorted, by name (A to Z or Z to A) or newest first by date modified or created, also set with the Sort By button at the top of each section * `notes.notesExtensions`: list of extensions recognized as notes or '*' for all extensions * `notes.watchExternalChanges`: update the Notes view when notes are added, renamed or removed outside of VS Code diff --git a/package.json b/package.json index 4ebde25..bb585b5 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,11 @@ "default": "md", "description": "The default extension to use for new notes. (do not include the dot)" }, + "notes.newNoteName": { + "type": "string", + "default": "", + "markdownDescription": "Name filled in when you create a note, so `Enter` accepts it and typing replaces it. Leave it empty to start with an empty name. Use VS Code's snippet date and time variables, for example `${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} ${CURRENT_HOUR}-${CURRENT_MINUTE}` for `2026-10-07 14-35`. Avoid `:`, which file names can't contain on Windows." + }, "notes.notesExtensions": { "type": "string", "default": "*", diff --git a/src/extension.ts b/src/extension.ts index 908a96e..00a5929 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,6 +8,7 @@ import { NotesViewProvider } from './notesViewProvider'; import { NotesWatcher } from './notesWatcher'; import { getLanguageExtensions, isMarkdownFile, resetLanguageExtensions } from './languages'; import { RecentNotes } from './recentNotes'; +import { expandNameTemplate } from './noteName'; import { compareEntries, readSortEntry, SortOrder, toSortOrder } from './sortOrder'; import { MovableItem, NotesDragAndDrop } from './notesDragAndDrop'; import { getLocationState, getMoveDestination, getPortableFolder, isInside, isNoteName, resolveNotesLocation, splitNoteName, toWorkspaceSetting } from './location'; @@ -583,10 +584,13 @@ export class Notes { if (!notesLocation) { return; } - // prompt user for a new note name + // prompt user for a new note name, filled in from the notes.newNoteName template when there is one, + // selected so Enter accepts it and typing replaces it + const suggested = expandNameTemplate(String(vscode.workspace.getConfiguration('notes').get('newNoteName') ?? ''), new Date(), vscode.env.language); const noteName = await vscode.window.showInputBox({ prompt: 'Note name?', - value: '', + value: suggested, + valueSelection: [0, suggested.length], }); if (noteName) { await Notes.createNote(notesLocation, noteName, tree); diff --git a/src/noteName.ts b/src/noteName.ts new file mode 100644 index 0000000..4eee575 --- /dev/null +++ b/src/noteName.ts @@ -0,0 +1,29 @@ +// fill in a name template for new notes with VS Code's snippet date and time variables, like +// ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}; $CURRENT_YEAR works too, and other text is kept as typed +export function expandNameTemplate(template: string, date: Date, locale: string = 'en'): string { + const pad = (value: number, length: number = 2) => String(value).padStart(length, '0'); + const name = (options: Intl.DateTimeFormatOptions) => date.toLocaleString(locale, options); + const offset = -date.getTimezoneOffset(); + const variables: Record string> = { + CURRENT_YEAR: () => String(date.getFullYear()), + CURRENT_YEAR_SHORT: () => String(date.getFullYear()).slice(-2), + CURRENT_MONTH: () => pad(date.getMonth() + 1), + CURRENT_MONTH_NAME: () => name({ month: 'long' }), + CURRENT_MONTH_NAME_SHORT: () => name({ month: 'short' }), + CURRENT_DATE: () => pad(date.getDate()), + CURRENT_DAY_NAME: () => name({ weekday: 'long' }), + CURRENT_DAY_NAME_SHORT: () => name({ weekday: 'short' }), + CURRENT_HOUR: () => pad(date.getHours()), + CURRENT_MINUTE: () => pad(date.getMinutes()), + CURRENT_SECOND: () => pad(date.getSeconds()), + CURRENT_MILLISECOND: () => pad(date.getMilliseconds(), 3), + CURRENT_SECONDS_UNIX: () => String(Math.floor(date.getTime() / 1000)), + CURRENT_MILLISECONDS_UNIX: () => String(date.getTime()), + CURRENT_TIMEZONE_OFFSET: () => `${offset >= 0 ? '+' : '-'}${pad(Math.floor(Math.abs(offset) / 60))}:${pad(Math.abs(offset) % 60)}`, + CURRENT_TIMEZONE_NAME: () => Intl.DateTimeFormat().resolvedOptions().timeZone, + }; + return template.replace(/\$\{([A-Z_]+)\}|\$([A-Z_]+)/g, (match, braced: string | undefined, bare: string | undefined) => { + const variable = variables[braced ?? bare ?? '']; + return variable ? variable() : match; + }); +} diff --git a/src/test/suite/noteName.test.ts b/src/test/suite/noteName.test.ts new file mode 100644 index 0000000..e097840 --- /dev/null +++ b/src/test/suite/noteName.test.ts @@ -0,0 +1,33 @@ +import * as assert from 'assert'; +import { expandNameTemplate } from '../../noteName'; + +suite('New Note Name (#61)', () => { + // Wednesday 7 October 2026, 09:05:03.042 local time + const date = new Date(2026, 9, 7, 9, 5, 3, 42); + + test('date and time variables are filled in, two digits each', () => { + assert.strictEqual(expandNameTemplate('${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} ${CURRENT_HOUR}-${CURRENT_MINUTE}-${CURRENT_SECOND}', date), '2026-10-07 09-05-03'); + }); + + test('the short year and the milliseconds', () => { + assert.strictEqual(expandNameTemplate('${CURRENT_YEAR_SHORT}.${CURRENT_MILLISECOND}', date), '26.042'); + }); + + test('month and day names', () => { + assert.strictEqual(expandNameTemplate('${CURRENT_DAY_NAME} ${CURRENT_DATE} ${CURRENT_MONTH_NAME}', date), 'Wednesday 07 October'); + assert.strictEqual(expandNameTemplate('${CURRENT_DAY_NAME_SHORT} ${CURRENT_MONTH_NAME_SHORT}', date), 'Wed Oct'); + }); + + test('variables without braces work too, and the longest name wins', () => { + assert.strictEqual(expandNameTemplate('$CURRENT_YEAR_SHORT$CURRENT_MONTH notes', date), '2610 notes'); + }); + + test('unix time', () => { + assert.strictEqual(expandNameTemplate('${CURRENT_SECONDS_UNIX}', date), String(Math.floor(date.getTime() / 1000))); + }); + + test('other text and unknown variables are kept as typed', () => { + assert.strictEqual(expandNameTemplate('Meeting ${CURRENT_DATE} with $client ${NOT_A_VARIABLE}', date), 'Meeting 07 with $client ${NOT_A_VARIABLE}'); + assert.strictEqual(expandNameTemplate('', date), ''); + }); +});