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 @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

* `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)

### Changed

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Notes are stored in a folder anywhere on your system you'd like, with as many su
The extension can be accessed using the Notes icon that is placed in the Activity Bar, or in the Command Palette (CMD+Shift+P or CTRL+Shift+P) by typing `Notes`.

* 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. If what you type isn't a note, press `Enter` to create it.
* 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.
* 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.
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@
"title": "List Notes",
"category": "Notes"
},
{
"command": "Notes.clearRecentlyOpened",
"title": "Clear Recently Opened",
"category": "Notes"
},
{
"command": "Notes.newNote",
"title": "New Note",
Expand Down
41 changes: 37 additions & 4 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Note } from './note';
import { NotesViewProvider } from './notesViewProvider';
import { NotesWatcher } from './notesWatcher';
import { getLanguageExtensions, isMarkdownFile, resetLanguageExtensions } from './languages';
import { RecentNotes } from './recentNotes';
import { MovableItem, NotesDragAndDrop } from './notesDragAndDrop';
import { getLocationState, getMoveDestination, getPortableFolder, isInside, isNoteName, resolveNotesLocation, splitNoteName, toWorkspaceSetting } from './location';

Expand Down Expand Up @@ -120,12 +121,34 @@ export function activate(context: vscode.ExtensionContext) {
});
context.subscriptions.push(deleteFolderDisposable);

// remember the notes opened, from Notes or anywhere else, for List Notes
const recentNotes = new RecentNotes(context.globalState);
const rememberActiveNote = () => {
const input = vscode.window.tabGroups.activeTabGroup.activeTab?.input;
const uri = input instanceof vscode.TabInputText || input instanceof vscode.TabInputCustom ? input.uri : undefined;
if (uri?.scheme === 'file' && [notesTree.location, workspaceTree.location].some(location => location && isInside(uri.fsPath, location) && uri.fsPath !== location)) {
recentNotes.add(uri.fsPath);
}
};
context.subscriptions.push(
vscode.window.tabGroups.onDidChangeTabs(rememberActiveNote),
vscode.window.tabGroups.onDidChangeTabGroups(rememberActiveNote),
vscode.workspace.onDidRenameFiles(e => e.files.forEach(file => recentNotes.rename(file.oldUri.fsPath, file.newUri.fsPath)))
);

// list notes
let listNotesDisposable = vscode.commands.registerCommand('Notes.listNotes', () => {
Notes.listNotes(trees);
Notes.listNotes(trees, recentNotes);
});
context.subscriptions.push(listNotesDisposable);

// forget the recently opened notes
let clearRecentNotesDisposable = vscode.commands.registerCommand('Notes.clearRecentlyOpened', async () => {
await recentNotes.clear();
vscode.window.showInformationMessage('Cleared the recently opened notes.');
});
context.subscriptions.push(clearRecentNotesDisposable);

// new note, in the folder of the given item, or asking where when the workspace has notes too
let newNoteDisposable = vscode.commands.registerCommand('Notes.newNote', (item?: Note) => {
Notes.newNote(trees, undefined, item);
Expand Down Expand Up @@ -437,7 +460,7 @@ export class Notes {

// list notes, from Workspace Notes and Notes and the folders in them; typing a name that isn't a note
// offers to create it, so Enter creates the note when nothing matches (#34)
static async listNotes(tree: Refreshable): Promise<void> {
static async listNotes(tree: Refreshable, recentNotes: RecentNotes): Promise<void> {
const sources = [{ label: 'Notes', location: Notes.getNotesLocation() }];
if (Notes.isWorkspaceNotesAvailable()) {
sources.unshift({ label: 'Workspace Notes', location: Notes.getWorkspaceNotesLocation() });
Expand All @@ -459,10 +482,19 @@ export class Notes {
}
}

// notes opened recently come first, newest first, like Quick Open; the others stay in name order
const recent = recentNotes.get()
.map(filePath => notes.find(note => note.filePath === filePath))
.filter((note): note is NoteItem => !!note);
const others = notes.filter(note => !recent.includes(note));
const listed: NoteItem[] = recent.length
? [{ label: 'recently opened', kind: vscode.QuickPickItemKind.Separator }, ...recent, { label: 'other notes', kind: vscode.QuickPickItemKind.Separator }, ...others]
: notes;

const quickPick = vscode.window.createQuickPick<NoteItem>();
quickPick.placeholder = 'Open a note, or type a name to create one';
quickPick.matchOnDescription = true;
quickPick.items = notes;
quickPick.items = listed;
quickPick.onDidChangeValue(value => {
const name = value.trim();
// offer to create the typed name in each section, unless a note already has that name
Expand All @@ -472,7 +504,8 @@ export class Notes {
alwaysShow: true,
createIn: source.location
}));
quickPick.items = [...notes, ...create];
// while typing, VS Code ranks the matches itself, so the headings would label the wrong notes
quickPick.items = name ? [...notes, ...create] : listed;
});
quickPick.onDidAccept(async () => {
const picked = quickPick.activeItems[0];
Expand Down
48 changes: 48 additions & 0 deletions src/recentNotes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as path from 'path';

// the storage the history is kept in, the extension's global state in VS Code
export interface HistoryStore {
get<T>(key: string, defaultValue: T): T;
update(key: string, value: unknown): Thenable<void>;
}

const KEY = 'recentlyOpenedNotes';
const LIMIT = 50;

// the notes opened most recently, newest first, like the recently opened files of Quick Open;
// kept on this machine only, never in settings or the notes folder
export class RecentNotes {

constructor(private store: HistoryStore, private limit: number = LIMIT) {
}

// note paths, newest first
get(): string[] {
return this.store.get<string[]>(KEY, []);
}

// a note was opened: put it first
async add(filePath: string): Promise<void> {
const recent = this.get();
if (recent[0] === filePath) {
return;
}
await this.store.update(KEY, [filePath, ...recent.filter(recentPath => recentPath !== filePath)].slice(0, this.limit));
}

// a note or folder was moved or renamed: follow it
async rename(oldPath: string, newPath: string): Promise<void> {
const recent = this.get();
const renamed = recent.map(recentPath =>
recentPath === oldPath ? newPath
: recentPath.startsWith(oldPath + path.sep) ? newPath + recentPath.slice(oldPath.length)
: recentPath);
if (renamed.some((renamedPath, index) => renamedPath !== recent[index])) {
await this.store.update(KEY, renamed);
}
}

async clear(): Promise<void> {
await this.store.update(KEY, []);
}
}
58 changes: 58 additions & 0 deletions src/test/suite/recentNotes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as assert from 'assert';
import * as path from 'path';
import { HistoryStore, RecentNotes } from '../../recentNotes';

// the extension's global state, kept in memory
class MemoryStore implements HistoryStore {
private values = new Map<string, unknown>();
get<T>(key: string, defaultValue: T): T {
return this.values.has(key) ? this.values.get(key) as T : defaultValue;
}
async update(key: string, value: unknown): Promise<void> {
this.values.set(key, value);
}
}

suite('Recently Opened Notes (#18)', () => {
const notes = path.join(path.sep, 'notes');
const note = (...parts: string[]) => path.join(notes, ...parts);

test('the note opened last comes first, without duplicates', async () => {
const recent = new RecentNotes(new MemoryStore());
await recent.add(note('zz.md'));
await recent.add(note('todo.md'));
await recent.add(note('zz.md'));
assert.deepStrictEqual(recent.get(), [note('zz.md'), note('todo.md')]);
});

test('only the most recent notes are kept', async () => {
const recent = new RecentNotes(new MemoryStore(), 3);
for (const name of ['a', 'b', 'c', 'd']) {
await recent.add(note(`${name}.md`));
}
assert.deepStrictEqual(recent.get(), [note('d.md'), note('c.md'), note('b.md')]);
});

test('a moved note keeps its place', async () => {
const recent = new RecentNotes(new MemoryStore());
await recent.add(note('todo.md'));
await recent.add(note('zz.md'));
await recent.rename(note('todo.md'), note('archive', 'todo.md'));
assert.deepStrictEqual(recent.get(), [note('zz.md'), note('archive', 'todo.md')]);
});

test('notes in a renamed folder follow it, notes in a folder with a similar name do not', async () => {
const recent = new RecentNotes(new MemoryStore());
await recent.add(note('work-old', 'a.md'));
await recent.add(note('work', 'b.md'));
await recent.rename(note('work'), note('projects'));
assert.deepStrictEqual(recent.get(), [note('projects', 'b.md'), note('work-old', 'a.md')]);
});

test('clearing forgets every note', async () => {
const recent = new RecentNotes(new MemoryStore());
await recent.add(note('todo.md'));
await recent.clear();
assert.deepStrictEqual(recent.get(), []);
});
});
Loading