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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ 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 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

* Refresh and Settings moved from the title bar of Notes and Workspace Notes to its `...` menu
* notes open like they do from the Explorer, with the editor VS Code associates with their file type: images open in the image viewer, and Markdown opens in the preview or another Markdown editor when `workbench.editorAssociations` says so (#53, #70); a new note still opens as text

### Fixed
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ The extension can be accessed using the Notes icon that is placed in the Activit
* quickly create new notes by using the `Alt+N` shortcut, or by clicking the New Note icon at the top of Notes.
* quickly access your list of notes by using the `Alt+L` shortcut to bring up a searchable list at the top of VSCode, including the notes in folders, with the notes you opened recently first. If what you type isn't a note, press `Enter` to create it.
* search inside your notes with **Notes: Search Notes**, the search button at the top of each section, or **Search in Folder** in the right-click menu of a folder. It opens VS Code's Search view limited to your notes, wherever they are stored.
* sort notes by name or by date modified or created with the Sort By button at the top of Notes.
* right-click a note or folder to rename, delete, move or search it, or to reveal it in your file explorer, and a Markdown note to open its preview. *Deleting a note is permanent, so be careful.*
* notes open the way they do from the Explorer, with the editor VS Code uses for their file type, so images open in the image viewer.
* move notes and folders by dragging them onto a folder (or onto empty space for the top level), between Workspace Notes and Notes, or with **Move To...** in the right-click menu. Select several with `Cmd`/`Ctrl` to move them together, and drag a note onto the editor to open it.

## Getting Started

Notes will prompt you for a storage location the first time you access the extension from the Activity Bar or through the Command Palette. If you would like to change the storage location, later on, you can access the Notes extension settings by clicking on the gear icon in Notes or from the Command Palette. After you've selected a storage location, you can access your notes from the Notes icon in the Activity Bar, or through the Command Palette.
Notes will prompt you for a storage location the first time you access the extension from the Activity Bar or through the Command Palette. If you would like to change the storage location, later on, you can access the Notes extension settings from **Settings** in the `...` menu at the top of Notes, or from the Command Palette. After you've selected a storage location, you can access your notes from the Notes icon in the Activity Bar, or through the Command Palette.

## Workspace Notes

Expand All @@ -44,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.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
* `notes.watchIntervalSeconds`: how often the open folders are checked for those changes while the Notes view is visible
Expand Down
39 changes: 36 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@
"default": "*",
"description": "A comma separated list of allowed extensions for notes. Use '*' (asterisk) to allow all file types. (do not include the dot or spaces)"
},
"notes.sortOrder": {
"type": "string",
"default": "name",
"enum": [
"name",
"nameDescending",
"modified",
"created"
],
"enumDescriptions": [
"By name, A to Z.",
"By name, Z to A.",
"By date modified, newest first.",
"By date created, newest first. Where the file system does not record it, the date modified is used."
],
"description": "How notes and folders are sorted in Notes and Workspace Notes, and the notes after the recently opened ones in List Notes. Folders always come first."
},
"notes.watchExternalChanges": {
"type": "boolean",
"default": true,
Expand Down Expand Up @@ -156,6 +173,12 @@
"title": "Search in Folder",
"category": "Notes"
},
{
"command": "Notes.sortBy",
"title": "Sort By...",
"category": "Notes",
"icon": "$(sort-precedence)"
},
{
"command": "Notes.refreshNotes",
"title": "Refresh Notes",
Expand Down Expand Up @@ -260,10 +283,15 @@
"group": "navigation@3"
},
{
"command": "Notes.refreshNotes",
"command": "Notes.sortBy",
"when": "view == workspaceNotes",
"group": "navigation@4"
},
{
"command": "Notes.refreshNotes",
"when": "view == workspaceNotes",
"group": "3_view@1"
},
{
"command": "Notes.revealWorkspaceNotesInOS",
"when": "view == workspaceNotes && (!remoteName || remoteName == wsl)",
Expand All @@ -290,14 +318,19 @@
"group": "navigation@3"
},
{
"command": "Notes.refreshNotes",
"command": "Notes.sortBy",
"when": "view == notes",
"group": "navigation@4"
},
{
"command": "Notes.refreshNotes",
"when": "view == notes",
"group": "3_view@1"
},
{
"command": "Notes.setupNotes",
"when": "view == notes",
"group": "navigation@5"
"group": "4_settings@1"
},
{
"command": "Notes.revealInOS",
Expand Down
58 changes: 56 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 { 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 @@ -84,7 +85,8 @@ export function activate(context: vscode.ExtensionContext) {
// apply a new storage location or list of extensions right away
if (e.affectsConfiguration('notes.notesLocation')
|| e.affectsConfiguration('notes.workspaceNotesLocation')
|| e.affectsConfiguration('notes.notesExtensions')) {
|| e.affectsConfiguration('notes.notesExtensions')
|| e.affectsConfiguration('notes.sortOrder')) {
updateTrees(true);
}
// apply new watch settings
Expand Down Expand Up @@ -142,6 +144,12 @@ export function activate(context: vscode.ExtensionContext) {
});
context.subscriptions.push(listNotesDisposable);

// choose how notes are sorted
let sortByDisposable = vscode.commands.registerCommand('Notes.sortBy', () => {
Notes.pickSortOrder();
});
context.subscriptions.push(sortByDisposable);

// forget the recently opened notes
let clearRecentNotesDisposable = vscode.commands.registerCommand('Notes.clearRecentlyOpened', async () => {
await recentNotes.clear();
Expand Down Expand Up @@ -486,10 +494,16 @@ export class Notes {
const recent = recentNotes.get()
.map(filePath => notes.find(note => note.filePath === filePath))
.filter((note): note is NoteItem => !!note);
// the other notes in the order chosen for the tree; by name they stay grouped by folder
const order = toSortOrder(vscode.workspace.getConfiguration('notes').get('sortOrder'));
const others = notes.filter(note => !recent.includes(note));
if (order !== 'name') {
const entries = new Map(others.map(note => [note, readSortEntry(note.filePath!, false, order)]));
others.sort((a, b) => compareEntries(entries.get(a)!, entries.get(b)!, order));
}
const listed: NoteItem[] = recent.length
? [{ label: 'recently opened', kind: vscode.QuickPickItemKind.Separator }, ...recent, { label: 'other notes', kind: vscode.QuickPickItemKind.Separator }, ...others]
: notes;
: others;

const quickPick = vscode.window.createQuickPick<NoteItem>();
quickPick.placeholder = 'Open a note, or type a name to create one';
Expand Down Expand Up @@ -522,6 +536,46 @@ export class Notes {
quickPick.show();
}

// choose how notes and folders are sorted, saved in the notes.sortOrder setting
static pickSortOrder(): void {
const config = vscode.workspace.getConfiguration('notes');
const current = toSortOrder(config.get('sortOrder'));
const options: { order: SortOrder, label: string, description: string }[] = [
{ order: 'name', label: 'Name', description: 'A to Z' },
{ order: 'nameDescending', label: 'Name', description: 'Z to A' },
{ order: 'modified', label: 'Date Modified', description: 'newest first' },
{ order: 'created', label: 'Date Created', description: 'newest first' },
];
type SortItem = vscode.QuickPickItem & { order: SortOrder };
// menus can't show a check mark for an extension, the list marks the current order instead
const items: SortItem[] = options.map(option => ({
label: `${option.order === current ? '$(check)' : '$(blank)'} ${option.label}`,
description: option.description,
order: option.order
}));

const quickPick = vscode.window.createQuickPick<SortItem>();
quickPick.placeholder = 'Sort notes by';
quickPick.items = items;
quickPick.activeItems = items.filter(item => item.order === current);
quickPick.onDidAccept(() => {
const picked = quickPick.activeItems[0];
quickPick.hide();
if (picked && picked.order !== current) {
// where the setting is in effect, the workspace settings when they set it
const target = config.inspect('sortOrder')?.workspaceValue !== undefined
? vscode.ConfigurationTarget.Workspace
: vscode.ConfigurationTarget.Global;
config.update('sortOrder', picked.order, target).then(undefined, err => {
console.error(err);
vscode.window.showErrorMessage('Failed to save the sort order.');
});
}
});
quickPick.onDidHide(() => quickPick.dispose());
quickPick.show();
}

// new note
static async newNote(tree: Refreshable, kind?: NotesKind, item?: Note): Promise<void> {
// Determine the location where the note should be created
Expand Down
15 changes: 5 additions & 10 deletions src/notesViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as gl from 'glob';
import * as path from 'path';
import { Note } from './note';
import { LocationState, getLocationState } from './location';
import { compareEntries, readSortEntry, toSortOrder } from './sortOrder';

export class NotesViewProvider implements vscode.TreeDataProvider<Note> {

Expand Down Expand Up @@ -169,16 +170,10 @@ export class NotesViewProvider implements vscode.TreeDataProvider<Note> {
console.error('Error reading directory:', err);
}

// Sort: folders first, then notes alphabetically
result.sort((a, b) => {
if (a.isFolder && !b.isFolder) {
return -1;
}
if (!a.isFolder && b.isFolder) {
return 1;
}
return a.name.localeCompare(b.name);
});
// Sort: folders first, then by the notes.sortOrder setting
const order = toSortOrder(vscode.workspace.getConfiguration('notes').get('sortOrder'));
const entries = new Map(result.map(note => [note, readSortEntry(note.fullPath, note.isFolder, order)]));
result.sort((a, b) => compareEntries(entries.get(a)!, entries.get(b)!, order));

return result;
}
Expand Down
59 changes: 59 additions & 0 deletions src/sortOrder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as fs from 'fs';
import * as path from 'path';

// how notes and folders are ordered: by name, A to Z or Z to A, or newest first by date modified or created;
// folders always come first, as in the Explorer
export type SortOrder = 'name' | 'nameDescending' | 'modified' | 'created';

const SORT_ORDERS: SortOrder[] = ['name', 'nameDescending', 'modified', 'created'];

// what the order compares, dates in milliseconds
export interface SortEntry {
name: string;
isFolder: boolean;
modified: number;
created: number;
}

// the notes.sortOrder setting, name for anything it doesn't know
export function toSortOrder(value: unknown): SortOrder {
return SORT_ORDERS.includes(value as SortOrder) ? value as SortOrder : 'name';
}

// does this order need the dates of files and folders?
export function needsDates(order: SortOrder): boolean {
return order === 'modified' || order === 'created';
}

export function compareEntries(a: SortEntry, b: SortEntry, order: SortOrder): number {
if (a.isFolder !== b.isFolder) {
return a.isFolder ? -1 : 1;
}
switch (order) {
case 'nameDescending':
return b.name.localeCompare(a.name);
// newest first, then by name for the same date
case 'modified':
return b.modified - a.modified || a.name.localeCompare(b.name);
case 'created':
return b.created - a.created || a.name.localeCompare(b.name);
default:
return a.name.localeCompare(b.name);
}
}

// the name and, when the order needs them, the dates of a file or folder
export function readSortEntry(fullPath: string, isFolder: boolean, order: SortOrder): SortEntry {
const entry: SortEntry = { name: path.basename(fullPath), isFolder, modified: 0, created: 0 };
if (needsDates(order)) {
try {
const stat = fs.statSync(fullPath);
entry.modified = stat.mtimeMs;
// some file systems don't record when a file was created, the date modified is the next best thing
entry.created = stat.birthtimeMs || stat.mtimeMs;
} catch (err) {
// gone since it was listed, it sorts last
}
}
return entry;
}
40 changes: 40 additions & 0 deletions src/test/suite/sortOrder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as assert from 'assert';
import { compareEntries, SortEntry, SortOrder, toSortOrder } from '../../sortOrder';

suite('Sort Order (#22, #63)', () => {
const entries: SortEntry[] = [
{ name: 'beta.md', isFolder: false, modified: 300, created: 100 },
{ name: 'alpha.md', isFolder: false, modified: 100, created: 300 },
{ name: 'gamma.md', isFolder: false, modified: 200, created: 200 },
{ name: 'work', isFolder: true, modified: 50, created: 50 },
{ name: 'archive', isFolder: true, modified: 400, created: 10 },
];
const sorted = (order: SortOrder) => [...entries].sort((a, b) => compareEntries(a, b, order)).map(entry => entry.name);

test('by name, folders first', () => {
assert.deepStrictEqual(sorted('name'), ['archive', 'work', 'alpha.md', 'beta.md', 'gamma.md']);
});

test('by name from Z to A, folders still first', () => {
assert.deepStrictEqual(sorted('nameDescending'), ['work', 'archive', 'gamma.md', 'beta.md', 'alpha.md']);
});

test('by date modified, newest first', () => {
assert.deepStrictEqual(sorted('modified'), ['archive', 'work', 'beta.md', 'gamma.md', 'alpha.md']);
});

test('by date created, newest first', () => {
assert.deepStrictEqual(sorted('created'), ['work', 'archive', 'alpha.md', 'gamma.md', 'beta.md']);
});

test('the same date falls back to the name', () => {
const same: SortEntry[] = [{ name: 'b.md', isFolder: false, modified: 1, created: 1 }, { name: 'a.md', isFolder: false, modified: 1, created: 1 }];
assert.deepStrictEqual(same.sort((a, b) => compareEntries(a, b, 'modified')).map(entry => entry.name), ['a.md', 'b.md']);
});

test('an unknown setting sorts by name', () => {
assert.strictEqual(toSortOrder('size'), 'name');
assert.strictEqual(toSortOrder(undefined), 'name');
assert.strictEqual(toSortOrder('modified'), 'modified');
});
});
Loading