Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "*",
Expand Down
8 changes: 6 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
29 changes: 29 additions & 0 deletions src/noteName.ts
Original file line number Diff line number Diff line change
@@ -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, () => 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;
});
}
33 changes: 33 additions & 0 deletions src/test/suite/noteName.test.ts
Original file line number Diff line number Diff line change
@@ -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), '');
});
});
Loading