From e2ffd39b343b329e8d762a208a38e090eb2e66a6 Mon Sep 17 00:00:00 2001 From: Dion Munk Date: Wed, 7 Oct 2026 08:48:18 -0600 Subject: [PATCH] feat(notes): open notes like the Explorer and add Open Preview Notes always opened with showTextDocument, which forces the text editor: it ignored workbench.editorAssociations, so Markdown associated with the preview or another editor still opened as text (#10, #53), and an image failed to open at all (#70). - notes open with vscode.open, like the Explorer, from the tree and List Notes; a new note still opens as text, ready to type in - Open Preview in the right-click menu of a Markdown note, as the Explorer has; a tree item gets no language context, so Markdown notes have their own context value and the note menu entries match both - the languages VS Code knows move to their own module, read once and again when extensions change, and are shared by new notes and the tree --- CHANGELOG.md | 12 ++++++++++++ README.md | 3 ++- package.json | 22 ++++++++++++++++++---- src/extension.ts | 47 +++++++++++++++++++++++------------------------ src/languages.ts | 35 +++++++++++++++++++++++++++++++++++ src/note.ts | 6 ++++-- 6 files changed, 94 insertions(+), 31 deletions(-) create mode 100644 src/languages.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 014441e..5be1336 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 2d21455..7a7038e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index c5603f2..d5e1f13 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,11 @@ "category": "Notes", "icon": "$(refresh)" }, + { + "command": "Notes.openPreview", + "title": "Open Preview", + "category": "Notes" + }, { "command": "Notes.renameNote", "title": "Rename Note", @@ -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" }, { @@ -323,7 +333,7 @@ }, { "command": "Notes.move", - "when": "view =~ /^(notes|workspaceNotes)$/ && viewItem == note", + "when": "view =~ /^(notes|workspaceNotes)$/ && viewItem =~ /^(note|markdownNote)$/", "group": "1_modification@3" }, { @@ -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" }, { @@ -459,6 +469,10 @@ { "command": "Notes.deleteFolder", "when": "false" + }, + { + "command": "Notes.openPreview", + "when": "false" } ] }, diff --git a/src/extension.ts b/src/extension.ts index f5f5cc0..1b44427 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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'; @@ -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) { @@ -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); @@ -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 { - // plain text is registered by VS Code itself rather than by an extension - const languages = new Map([['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'); @@ -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); @@ -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 { - 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 @@ -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 @@ -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 diff --git a/src/languages.ts b/src/languages.ts new file mode 100644 index 0000000..f7668ca --- /dev/null +++ b/src/languages.ts @@ -0,0 +1,35 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; + +let languages: Map | 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 { + if (!languages) { + // plain text is registered by VS Code itself rather than by an extension + languages = new Map([['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); +} diff --git a/src/note.ts b/src/note.ts index 62bbe5e..c7f8ef7 100644 --- a/src/note.ts +++ b/src/note.ts @@ -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; @@ -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;