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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

* `Open Preview` in the right-click menu of a Markdown note, like in the Explorer (#10)

### Changed

* 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

* clicking an image in Notes did nothing

## [2.1.0] - 2026-10-07

### Added
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ 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. 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. *Deleting a note is permanent, so be careful.*
* 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
Expand Down
22 changes: 18 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@
"category": "Notes",
"icon": "$(refresh)"
},
{
"command": "Notes.openPreview",
"title": "Open Preview",
"category": "Notes"
},
{
"command": "Notes.renameNote",
"title": "Rename Note",
Expand Down Expand Up @@ -301,14 +306,19 @@
}
],
"view/item/context": [
{
"command": "Notes.openPreview",
"when": "view =~ /^(notes|workspaceNotes)$/ && viewItem == markdownNote && !hasCustomMarkdownPreview",
"group": "navigation"
},
{
"command": "Notes.renameNote",
"when": "view =~ /^(notes|workspaceNotes)$/ && viewItem == note",
"when": "view =~ /^(notes|workspaceNotes)$/ && viewItem =~ /^(note|markdownNote)$/",
"group": "1_modification@1"
},
{
"command": "Notes.deleteNote",
"when": "view =~ /^(notes|workspaceNotes)$/ && viewItem == note",
"when": "view =~ /^(notes|workspaceNotes)$/ && viewItem =~ /^(note|markdownNote)$/",
"group": "1_modification@2"
},
{
Expand All @@ -323,7 +333,7 @@
},
{
"command": "Notes.move",
"when": "view =~ /^(notes|workspaceNotes)$/ && viewItem == note",
"when": "view =~ /^(notes|workspaceNotes)$/ && viewItem =~ /^(note|markdownNote)$/",
"group": "1_modification@3"
},
{
Expand Down Expand Up @@ -378,7 +388,7 @@
},
{
"command": "Notes.revealInOS",
"when": "view =~ /^(notes|workspaceNotes)$/ && viewItem == note && (!remoteName || remoteName == wsl)",
"when": "view =~ /^(notes|workspaceNotes)$/ && viewItem =~ /^(note|markdownNote)$/ && (!remoteName || remoteName == wsl)",
"group": "3_open@1"
},
{
Expand Down Expand Up @@ -459,6 +469,10 @@
{
"command": "Notes.deleteFolder",
"when": "false"
},
{
"command": "Notes.openPreview",
"when": "false"
}
]
},
Expand Down
47 changes: 23 additions & 24 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as path from 'path';
import { Note } from './note';
import { NotesViewProvider } from './notesViewProvider';
import { NotesWatcher } from './notesWatcher';
import { getLanguageExtensions, isMarkdownFile, resetLanguageExtensions } from './languages';
import { MovableItem, NotesDragAndDrop } from './notesDragAndDrop';
import { getLocationState, getMoveDestination, getPortableFolder, isInside, isNoteName, resolveNotesLocation, splitNoteName, toWorkspaceSetting } from './location';

Expand Down Expand Up @@ -93,6 +94,8 @@ export function activate(context: vscode.ExtensionContext) {
}),
// relative locations depend on the workspace folder
vscode.workspace.onDidChangeWorkspaceFolders(() => updateTrees(true)),
// a newly installed extension can add a language, like Markdown files with another extension
vscode.extensions.onDidChange(() => resetLanguageExtensions()),
// a workspace notes folder may have been created while the window was in the background
vscode.window.onDidChangeWindowState(state => {
if (state.focused) {
Expand Down Expand Up @@ -159,6 +162,12 @@ export function activate(context: vscode.ExtensionContext) {
});
context.subscriptions.push(refreshNotesDisposable);

// open the Markdown preview of a note
let openPreviewDisposable = vscode.commands.registerCommand('Notes.openPreview', (note: Note) => {
Notes.openPreview(note);
});
context.subscriptions.push(openPreviewDisposable);

// rename note
let renameNoteDisposable = vscode.commands.registerCommand('Notes.renameNote', (note: Note) => {
Notes.renameNote(note, trees);
Expand Down Expand Up @@ -362,24 +371,6 @@ export class Notes {
const target = kind ?? await Notes.pickKind();
return target ? Notes.requireLocation(target) : undefined;
}
// the file extensions VS Code has a language for, with the language, from every installed extension
// including the built-in ones
static getLanguageExtensions(): Map<string, string> {
// plain text is registered by VS Code itself rather than by an extension
const languages = new Map<string, string>([['txt', 'plaintext']]);
for (const extension of vscode.extensions.all) {
for (const language of extension.packageJSON?.contributes?.languages ?? []) {
for (const fileExtension of language.extensions ?? []) {
const key = String(fileExtension).replace(/^\./, '').toLowerCase();
if (!languages.has(key)) {
languages.set(key, language.id);
}
}
}
}
return languages;
}

// get notes default extension
static getNotesDefaultNoteExtension() {
return vscode.workspace.getConfiguration('notes').get('notesDefaultNoteExtension');
Expand Down Expand Up @@ -488,7 +479,7 @@ export class Notes {
const name = quickPick.value.trim();
quickPick.hide();
if (picked?.filePath) {
vscode.window.showTextDocument(vscode.Uri.file(picked.filePath));
Notes.openNote(picked.filePath);
}
else if (picked?.createIn && name) {
await Notes.createNote(picked.createIn, name, tree);
Expand Down Expand Up @@ -518,13 +509,13 @@ export class Notes {
// create a note in a folder and open it, keeping an extension typed with the name, like query.sql,
// otherwise using the default one
static async createNote(folder: string, noteName: string, tree: Refreshable): Promise<void> {
const languages = Notes.getLanguageExtensions();
const languages = getLanguageExtensions();
const { base, extension } = splitNoteName(noteName, String(Notes.getNotesExtensions()), new Set(languages.keys()));
const noteExtension = extension ?? String(Notes.getNotesDefaultNoteExtension());
// set note path
const filePath = path.join(folder, `${base.replace(/\:/gi, '')}.${noteExtension}`);
// a Markdown note starts with its name as a heading, other files start empty
const markdown = languages.get(noteExtension.toLowerCase()) === 'markdown' || ['md', 'markdown'].includes(noteExtension.toLowerCase());
const markdown = isMarkdownFile(`note.${noteExtension}`);
const firstLine = markdown ? `# ${base}\n\n` : '';

// if a note with name already exists
Expand Down Expand Up @@ -614,8 +605,16 @@ export class Notes {
filePath = path.join(String(note.location), String(note.name));
}

// Open the document
vscode.window.showTextDocument(vscode.Uri.file(filePath));
// open it like the Explorer does: with the editor associated with the file type, like the image viewer
// for an image, or the preview for Markdown when the user associated it with that
vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath));
}

// open the Markdown preview of a note, as Open Preview does in the Explorer
static openPreview(note: Note): void {
if (note instanceof Note && !note.isFolder) {
vscode.commands.executeCommand('markdown.showPreview', vscode.Uri.file(note.fullPath));
}
}

// reveal a note or folder in the file explorer, or a storage location when no item is given
Expand Down Expand Up @@ -677,7 +676,7 @@ export class Notes {

// use the new name as typed when it ends with an extension a note can have, like a.txt,
// otherwise keep the current extension, so a name like 'Meeting 2026.10.07' stays whole
const { extension } = splitNoteName(newNoteName, String(Notes.getNotesExtensions()), new Set(Notes.getLanguageExtensions().keys()));
const { extension } = splitNoteName(newNoteName, String(Notes.getNotesExtensions()), new Set(getLanguageExtensions().keys()));
let noteName: string = extension || !noteExtension ? newNoteName : `${newNoteName}.${noteExtension}`;

// check for existing note with the same name
Expand Down
35 changes: 35 additions & 0 deletions src/languages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as vscode from 'vscode';
import * as path from 'path';

let languages: Map<string, string> | undefined;

// the file extensions VS Code has a language for, with the language, from every installed extension
// including the built-in ones; read once and again when extensions change
export function getLanguageExtensions(): Map<string, string> {
if (!languages) {
// plain text is registered by VS Code itself rather than by an extension
languages = new Map<string, string>([['txt', 'plaintext']]);
for (const extension of vscode.extensions.all) {
for (const language of extension.packageJSON?.contributes?.languages ?? []) {
for (const fileExtension of language.extensions ?? []) {
const key = String(fileExtension).replace(/^\./, '').toLowerCase();
if (!languages.has(key)) {
languages.set(key, language.id);
}
}
}
}
}
return languages;
}

// forget the languages read so far, so newly installed extensions count
export function resetLanguageExtensions(): void {
languages = undefined;
}

// is a file a Markdown note?
export function isMarkdownFile(fileName: string): boolean {
const extension = path.extname(fileName).slice(1).toLowerCase();
return getLanguageExtensions().get(extension) === 'markdown' || ['md', 'markdown'].includes(extension);
}
6 changes: 4 additions & 2 deletions src/note.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { isMarkdownFile } from './languages';

export class Note extends vscode.TreeItem {
public readonly isFolder: boolean;
Expand All @@ -21,8 +22,9 @@ export class Note extends vscode.TreeItem {
this.resourceUri = vscode.Uri.file(this.fullPath);
this.iconPath = isDirectory ? vscode.ThemeIcon.Folder : vscode.ThemeIcon.File;

// Set contextValue based on whether this is a folder or note
this.contextValue = isDirectory ? 'folder' : 'note';
// Set contextValue based on whether this is a folder or note; Markdown notes get Open Preview,
// which the Explorer decides by language, a context a tree item doesn't get
this.contextValue = isDirectory ? 'folder' : isMarkdownFile(name) ? 'markdownNote' : 'note';
}

tooltip = this.name;
Expand Down
Loading