From bebd6c41811829b00e72b1a6249a281230eebdd2 Mon Sep 17 00:00:00 2001 From: Samer Murad Date: Wed, 15 Apr 2026 13:43:46 +0200 Subject: [PATCH 1/3] Add Mod+Shift+N shortcut to create scratch buffer without name prompt New Shortcut quickly creates a new scratch file without promoting the name/location dialog UI, Helpful for when you just want to have a new file created without bothering about name/location. Solution is pretty straightforward and actually pretty small, it just generates a `Scratch ${n}` name based on the length of current buffers. --- src/editor/commands.js | 5 +++++ src/editor/keymap.js | 3 ++- src/stores/heynote-store.js | 23 ++++++++++++++++++++--- tests/playwright/buffer-creation.spec.js | 14 ++++++++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/editor/commands.js b/src/editor/commands.js index bd826e55..5012d902 100644 --- a/src/editor/commands.js +++ b/src/editor/commands.js @@ -73,6 +73,10 @@ const openCreateNewBuffer = (editor) => () => { useHeynoteStore().openCreateBuffer("new") return true } +const createScratchBuffer = (editor) => () => { + useHeynoteStore().createScratchBuffer() + return true +} const closeCurrentTab = (editor) => () => { useHeynoteStore().closeCurrentTab() @@ -149,6 +153,7 @@ const HEYNOTE_COMMANDS = { openCommandPalette: cmd(openCommandPalette, "Editor", "Open command palette…"), openMoveToBuffer: cmd(openMoveToBuffer, "Block", "Move block to another buffer…"), openCreateNewBuffer: cmd(openCreateNewBuffer, "Buffer", "Create new buffer…"), + createScratchBuffer: cmd(createScratchBuffer, "Buffer", "Create new scratch buffer"), cut: cmd(cutCommand, "Clipboard", "Cut selection"), copy: cmd(copyCommand, "Clipboard", "Copy selection"), foldBlock: cmd(foldBlock, "Block", "Fold block"), diff --git a/src/editor/keymap.js b/src/editor/keymap.js index 72b78794..de3e7e2f 100644 --- a/src/editor/keymap.js +++ b/src/editor/keymap.js @@ -102,6 +102,7 @@ export const DEFAULT_KEYMAP = [ cmd("Mod-Shift-p", "openCommandPalette"), cmd("Mod-s", "openMoveToBuffer"), cmd("Mod-n", "openCreateNewBuffer"), + cmd("Mod-Shift-n", "createScratchBuffer"), cmd("Alt-Shift-f", "formatBlockContent"), cmd("Mod-Shift-Space", "toggleCheckbox"), @@ -299,7 +300,7 @@ export function getAllKeyBindingsForCommand(command, keymapName, userKeymap, ema "nothing", "toggleAlwaysOnTop", "toggleLeftPanel", - "openLanguageSelector", "openBufferSelector", "openCreateNewBuffer", "openMoveToBuffer", "openCommandPalette", + "openLanguageSelector", "openBufferSelector", "openCreateNewBuffer", "createScratchBuffer", "openMoveToBuffer", "openCommandPalette", "closeCurrentTab", "reopenLastClosedTab", "nextTab", "previousTab", "switchToTab1", "switchToTab2", "switchToTab3", "switchToTab4", "switchToTab5", "switchToTab6", "switchToTab7", "switchToTab8", "switchToTab9", "switchToLastTab" ]) diff --git a/src/stores/heynote-store.js b/src/stores/heynote-store.js index 6774ba3e..5236208d 100644 --- a/src/stores/heynote-store.js +++ b/src/stores/heynote-store.js @@ -1,12 +1,13 @@ import { toRaw, nextTick, watch } from 'vue'; import { defineStore } from "pinia" import { NoteFormat } from "../common/note-format" +import { filenameSlug } from "../common/sanitize-filename" import { toSafeBrowserLocale } from "../util/locale.js" import { useEditorCacheStore } from "./editor-cache" import { useSettingsStore } from "./settings-store" -import { - SCRATCH_FILE_NAME, WINDOW_FULLSCREEN_STATE, WINDOW_FOCUS_STATE, - SAVE_TABS_STATE, LOAD_TABS_STATE, CONTEXT_MENU_CLOSED +import { + SCRATCH_FILE_NAME, WINDOW_FULLSCREEN_STATE, WINDOW_FOCUS_STATE, + SAVE_TABS_STATE, LOAD_TABS_STATE, CONTEXT_MENU_CLOSED } from "../common/constants" @@ -285,6 +286,22 @@ export const useHeynoteStore = defineStore("heynote", { await toRaw(this.currentEditor).createNewBuffer(path, name) }, + /** + * Create a new buffer with an auto-generated "Scratch N" name (no modal). + * Picks the first N whose slugified path is not already taken. + */ + async createScratchBuffer() { + for (let n = 1; n < 10000; n++) { + const name = `Scratch ${n}` + const path = filenameSlug(name) + ".txt" + if (!this.buffers[path]) { + await this.createNewBuffer(path, name) + return + } + } + throw new Error("Could not find an available scratch buffer name") + }, + /** * Create a new note file at path, with name `name`, and content content * @param {*} path: File path relative to Heynote root diff --git a/tests/playwright/buffer-creation.spec.js b/tests/playwright/buffer-creation.spec.js index 56bde2ae..ddb050ab 100644 --- a/tests/playwright/buffer-creation.spec.js +++ b/tests/playwright/buffer-creation.spec.js @@ -125,6 +125,20 @@ Block C`) `)) }) +test("create scratch buffers via Mod+Shift+N without modal", async ({page}) => { + for (let i = 0; i < 3; i++) { + await page.locator("body").press(heynotePage.agnosticKey("Mod+Shift+N")) + await page.waitForTimeout(100) + await expect(page.locator(".new-buffer")).toHaveCount(0) + } + await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50) + + const buffers = Object.keys(await heynotePage.getStoredBufferList()) + expect(buffers).toContain("scratch-1.txt") + expect(buffers).toContain("scratch-2.txt") + expect(buffers).toContain("scratch-3.txt") +}) + test("folder selector hides dot-prefixed folders", async ({ page }) => { await page.evaluate(async () => { await window.heynote.buffer.createDirectory(".secret") From 2bcefe7e0c2d3a91b841d5075263d4d8277e1b55 Mon Sep 17 00:00:00 2001 From: Samer Murad Date: Wed, 15 Apr 2026 14:01:37 +0200 Subject: [PATCH 2/3] simplified createScratchBuffer slug name creation to just be "`Scratch ${Object.keys(this.buffers).length + 1}`" --- src/stores/heynote-store.js | 14 ++++---------- tests/playwright/buffer-creation.spec.js | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/stores/heynote-store.js b/src/stores/heynote-store.js index 5236208d..6b2e8fd8 100644 --- a/src/stores/heynote-store.js +++ b/src/stores/heynote-store.js @@ -288,18 +288,12 @@ export const useHeynoteStore = defineStore("heynote", { /** * Create a new buffer with an auto-generated "Scratch N" name (no modal). - * Picks the first N whose slugified path is not already taken. + * Name is derived from the current buffer count — purely a disposable identifier. */ async createScratchBuffer() { - for (let n = 1; n < 10000; n++) { - const name = `Scratch ${n}` - const path = filenameSlug(name) + ".txt" - if (!this.buffers[path]) { - await this.createNewBuffer(path, name) - return - } - } - throw new Error("Could not find an available scratch buffer name") + const name = `Scratch ${Object.keys(this.buffers).length + 1}` + const path = filenameSlug(name) + ".txt" + await this.createNewBuffer(path, name) }, /** diff --git a/tests/playwright/buffer-creation.spec.js b/tests/playwright/buffer-creation.spec.js index ddb050ab..a33df52a 100644 --- a/tests/playwright/buffer-creation.spec.js +++ b/tests/playwright/buffer-creation.spec.js @@ -134,9 +134,9 @@ test("create scratch buffers via Mod+Shift+N without modal", async ({page}) => { await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50) const buffers = Object.keys(await heynotePage.getStoredBufferList()) - expect(buffers).toContain("scratch-1.txt") expect(buffers).toContain("scratch-2.txt") expect(buffers).toContain("scratch-3.txt") + expect(buffers).toContain("scratch-4.txt") }) test("folder selector hides dot-prefixed folders", async ({ page }) => { From 0aa7c1fb04418ec081f2abfc9d930bd19c312a88 Mon Sep 17 00:00:00 2001 From: Samer Murad Date: Wed, 15 Apr 2026 14:46:25 +0200 Subject: [PATCH 3/3] createScratchBuffer takes max N of scratch-N to avoid naming confilicts --- src/stores/heynote-store.js | 9 +++++++-- tests/playwright/buffer-creation.spec.js | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/stores/heynote-store.js b/src/stores/heynote-store.js index 6b2e8fd8..91bfcb86 100644 --- a/src/stores/heynote-store.js +++ b/src/stores/heynote-store.js @@ -288,10 +288,15 @@ export const useHeynoteStore = defineStore("heynote", { /** * Create a new buffer with an auto-generated "Scratch N" name (no modal). - * Name is derived from the current buffer count — purely a disposable identifier. + * N is the highest existing Scratch index + 1, so deletions don't cause collisions. */ async createScratchBuffer() { - const name = `Scratch ${Object.keys(this.buffers).length + 1}` + const scratchPathRe = /^scratch-(\d+)\.txt$/ + const maxN = Object.keys(this.buffers).reduce((max, path) => { + const match = path.match(scratchPathRe) + return match ? Math.max(max, parseInt(match[1], 10)) : max + }, 0) + const name = `Scratch ${maxN + 1}` const path = filenameSlug(name) + ".txt" await this.createNewBuffer(path, name) }, diff --git a/tests/playwright/buffer-creation.spec.js b/tests/playwright/buffer-creation.spec.js index a33df52a..ddb050ab 100644 --- a/tests/playwright/buffer-creation.spec.js +++ b/tests/playwright/buffer-creation.spec.js @@ -134,9 +134,9 @@ test("create scratch buffers via Mod+Shift+N without modal", async ({page}) => { await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50) const buffers = Object.keys(await heynotePage.getStoredBufferList()) + expect(buffers).toContain("scratch-1.txt") expect(buffers).toContain("scratch-2.txt") expect(buffers).toContain("scratch-3.txt") - expect(buffers).toContain("scratch-4.txt") }) test("folder selector hides dot-prefixed folders", async ({ page }) => {