diff --git a/docs/changelog.md b/docs/changelog.md index 0affe86..166c2ad 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,7 @@ Here are the most notable changes in each release. For a more detailed list of c ## 2.1.0 (not yet released) +- Added support for moving the current block to another (or new) buffer. Pressing `Ctrl/Cmd+S` will now pop up a dialog where you can search for and select another buffer to which the block will be moved. It's also possible to select to create a brand new buffer to which the block will be moved. - Added support for the following languages: * Elixir * Scala diff --git a/docs/index.md b/docs/index.md index fefbced..e6fd843 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,7 +39,7 @@ Available for Mac, Windows, and Linux. ⌘ + ⌥ + Enter Split the current block at cursor position ⌘ + L Change block language ⌘ + N Create a new note buffer -⌘ + S Create a new note buffer from the current block +⌘ + S Move the current block to another (or new) buffer ⌘ + P Open note selector ⌘ + Down Goto next block ⌘ + Up Goto previous block @@ -58,7 +58,7 @@ Alt + Shift + Enter Add new block at the start of the buffer Ctrl + Alt + Enter Split the current block at cursor position Ctrl + L Change block language Ctrl + N Create a new note buffer -Ctrl + S Create a new note buffer from the current block +Ctrl + S Move the current block to another (or new) buffer Ctrl + P Open note selector Ctrl + Down Goto next block Ctrl + Up Goto previous block diff --git a/shared-utils/key-helper.ts b/shared-utils/key-helper.ts index 8a6ac0b..a07f3f6 100644 --- a/shared-utils/key-helper.ts +++ b/shared-utils/key-helper.ts @@ -10,7 +10,7 @@ export const keyHelpStr = (platform: string) => { [`${modChar} + ${altChar} + Enter`, "Split the current block at cursor position"], [`${modChar} + L`, "Change block language"], [`${modChar} + N`, "Create a new note buffer"], - [`${modChar} + S`, "Create a new note buffer from the current block"], + [`${modChar} + S`, "Move the current block to another (or new) buffer"], [`${modChar} + P`, "Open note selector"], [`${modChar} + Down`, "Goto next block"], [`${modChar} + Up`, "Goto previous block"], diff --git a/src/components/App.vue b/src/components/App.vue index dd3c680..64d599c 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -5,6 +5,7 @@ import { useHeynoteStore } from "../stores/heynote-store" import { useErrorStore } from "../stores/error-store" import { useSettingsStore } from "../stores/settings-store" + import { useEditorCacheStore } from '../stores/editor-cache' import { OPEN_SETTINGS_EVENT, SETTINGS_CHANGE_EVENT } from '@/src/common/constants' @@ -55,6 +56,7 @@ showBufferSelector(value) { this.dialogWatcher(value) }, showCreateBuffer(value) { this.dialogWatcher(value) }, showEditBuffer(value) { this.dialogWatcher(value) }, + showMoveToBufferSelector(value) { this.dialogWatcher(value) }, currentBufferPath() { this.focusEditor() @@ -66,7 +68,7 @@ }, computed: { - ...mapStores(useSettingsStore), + ...mapStores(useSettingsStore, useEditorCacheStore), ...mapState(useHeynoteStore, [ "currentBufferPath", "currentBufferName", @@ -74,6 +76,7 @@ "showBufferSelector", "showCreateBuffer", "showEditBuffer", + "showMoveToBufferSelector", ]), editorInert() { @@ -89,6 +92,7 @@ "closeDialog", "closeBufferSelector", "openBuffer", + "closeMoveToBufferSelector", ]), // Used as a watcher for the booleans that control the visibility of editor dialogs. @@ -123,6 +127,11 @@ formatCurrentBlock() { this.$refs.editor.formatCurrentBlock() }, + + onMoveCurrentBlockToOtherEditor(path) { + this.editorCacheStore.moveCurrentBlockToOtherEditor(path) + this.closeMoveToBufferSelector() + }, }, } @@ -157,8 +166,16 @@ +
+

{{headline}}

- import { HeynoteEditor } from '../editor/editor.js' import { syntaxTree } from "@codemirror/language" import { toRaw } from 'vue'; import { mapState, mapWritableState, mapActions, mapStores } from 'pinia' @@ -28,6 +27,9 @@ }, mounted() { + // initialize editorCacheStore (sets up watchers for settings changes, propagating them to all editors) + this.editorCacheStore.setUp(this.$refs.editor); + this.loadBuffer(this.currentBufferPath) // set up window close handler that will save the buffer and quit @@ -46,9 +48,6 @@ window.heynote.mainProcess.on(WINDOW_CLOSE_EVENT, this.onWindowClose) window.heynote.mainProcess.on(REDO_EVENT, this.onRedo) - // initialize editorCacheStore (sets up watchers for settings changes, propagating them to all editors) - this.editorCacheStore.setUp(); - // if debugSyntaxTree prop is set, display syntax tree for debugging if (this.debugSyntaxTree) { setInterval(() => { @@ -112,7 +111,7 @@ toRaw(this.editor).show() } else { //console.log("create new editor") - this.editor = this.editorCacheStore.createEditor(path, this.$refs.editor) + this.editor = this.editorCacheStore.createEditor(path) this.editorCacheStore.addEditor(path, toRaw(this.editor)) } diff --git a/src/components/NewBuffer.vue b/src/components/NewBuffer.vue index eac8d6c..74b31ed 100644 --- a/src/components/NewBuffer.vue +++ b/src/components/NewBuffer.vue @@ -91,7 +91,7 @@ }, dialogTitle() { - return this.createBufferParams.mode === "currentBlock" ? "New Buffer from Block" : "New Buffer" + return this.createBufferParams.mode === "currentBlock" ? "Move Block to New Buffer" : "New Buffer" }, }, diff --git a/src/editor/annotation.js b/src/editor/annotation.js index fae768e..2f6dc46 100644 --- a/src/editor/annotation.js +++ b/src/editor/annotation.js @@ -7,3 +7,4 @@ export const SET_CONTENT = "heynote-set-content" export const ADD_NEW_BLOCK = "heynote-add-new-block" export const DELETE_BLOCK = "heynote-delete-block" export const CURSOR_CHANGE = "heynote-cursor-change" +export const APPEND_BLOCK = "heynote-append-block" diff --git a/src/editor/block/block.js b/src/editor/block/block.js index 3e981a4..dd311a7 100644 --- a/src/editor/block/block.js +++ b/src/editor/block/block.js @@ -169,8 +169,9 @@ const blockLayer = layer({ idx++; return } - const fromCoordsTop = view.coordsAtPos(Math.max(block.content.from, view.visibleRanges[0].from)).top - let toCoordsBottom = view.coordsAtPos(Math.min(block.content.to, view.visibleRanges[view.visibleRanges.length - 1].to)).bottom + // view.coordsAtPos returns null if the editor is not visible + const fromCoordsTop = view.coordsAtPos(Math.max(block.content.from, view.visibleRanges[0].from))?.top + let toCoordsBottom = view.coordsAtPos(Math.min(block.content.to, view.visibleRanges[view.visibleRanges.length - 1].to))?.bottom if (idx === blocks.length - 1) { // Calculate how much extra height we need to add to the last block let extraHeight = view.viewState.editorHeight - ( diff --git a/src/editor/editor.js b/src/editor/editor.js index 69beb77..9539850 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -12,7 +12,7 @@ import { getFontTheme } from "./theme/font-theme.js"; import { customSetup } from "./setup.js" import { heynoteLang } from "./lang-heynote/heynote.js" import { noteBlockExtension, blockLineNumbers, blockState, getActiveNoteBlock, triggerCursorChange } from "./block/block.js" -import { heynoteEvent, SET_CONTENT, DELETE_BLOCK } from "./annotation.js"; +import { heynoteEvent, SET_CONTENT, DELETE_BLOCK, APPEND_BLOCK } from "./annotation.js"; import { changeCurrentBlockLanguage, triggerCurrenciesLoaded, getBlockDelimiter, deleteBlock } from "./block/commands.js" import { formatBlockContent } from "./block/format-code.js" import { heynoteKeymap } from "./keymap.js" @@ -127,7 +127,8 @@ export class HeynoteEditor { //this.setContent(content) this.setReadOnly(true) - this.loadContent().then(() => { + this.contentLoadedPromise = this.loadContent(); + this.contentLoadedPromise.then(() => { this.setReadOnly(false) }) @@ -166,7 +167,6 @@ export class HeynoteEditor { const content = await window.heynote.buffer.load(this.path) this.diskContent = content this.contentLoaded = true - this.setContent(content) // set up content change listener this.onChange = (content) => { @@ -174,6 +174,8 @@ export class HeynoteEditor { this.setContent(content) } window.heynote.buffer.addOnChangeCallback(this.path, this.onChange) + + await this.setContent(content) } setContent(content) { @@ -278,6 +280,10 @@ export class HeynoteEditor { this.notesStore.openCreateBuffer(createMode) } + openMoveToBufferSelector() { + this.notesStore.openMoveToBufferSelector() + } + async createNewBuffer(path, name) { const data = getBlockDelimiter(this.defaultBlockToken, this.defaultBlockAutoDetect) await this.notesStore.saveNewBuffer(path, name, data) @@ -302,8 +308,35 @@ export class HeynoteEditor { // by using requestAnimationFrame we avoid a race condition where rendering the block backgrounds // would fail if we immediately opened the new note (since the block UI wouldn't have time to update // after the block was deleted) - requestAnimationFrame(() => { - this.notesStore.openBuffer(path) + //requestAnimationFrame(() => { + // this.notesStore.openBuffer(path) + //}) + + // add new buffer to recent list so that it shows up at the top of the buffer selector + this.notesStore.addRecentBuffer(path) + this.notesStore.addRecentBuffer(this.notesStore.currentBufferPath) + } + + getActiveBlockContent() { + const block = getActiveNoteBlock(this.view.state) + if (!block) { + return + } + return this.view.state.sliceDoc(block.range.from, block.range.to) + } + + deleteActiveBlock() { + deleteBlock(this)(this.view) + } + + appendBlockContent(content) { + this.view.dispatch({ + changes: { + from: this.view.state.doc.length, + to: this.view.state.doc.length, + insert: content, + }, + annotations: [heynoteEvent.of(APPEND_BLOCK)], }) } diff --git a/src/editor/keymap.js b/src/editor/keymap.js index 15914ef..dabdfa3 100644 --- a/src/editor/keymap.js +++ b/src/editor/keymap.js @@ -59,7 +59,7 @@ export function heynoteKeymap(editor) { ["Alt-ArrowDown", moveLineDown], ["Mod-l", () => editor.openLanguageSelector()], ["Mod-p", () => editor.openBufferSelector()], - ["Mod-s", () => editor.openCreateBuffer("currentBlock")], + ["Mod-s", () => editor.openMoveToBufferSelector()], ["Mod-n", () => editor.openCreateBuffer("new")], ["Mod-Shift-d", deleteBlock(editor)], ["Alt-Shift-f", formatBlockContent], diff --git a/src/main.js b/src/main.js index dd29cd1..4d53fd6 100644 --- a/src/main.js +++ b/src/main.js @@ -7,7 +7,6 @@ import App from './components/App.vue' import { loadCurrencies } from './currency' import { useErrorStore } from './stores/error-store' import { useHeynoteStore, initHeynoteStore } from './stores/heynote-store' -import { useEditorCacheStore } from './stores/editor-cache' const pinia = createPinia() @@ -20,7 +19,6 @@ app.mount('#app').$nextTick(() => { }) const errorStore = useErrorStore() -const editorCacheStore = useEditorCacheStore() //errorStore.addError("test error") window.heynote.getInitErrors().then((errors) => { errors.forEach((e) => errorStore.addError(e)) diff --git a/src/stores/editor-cache.js b/src/stores/editor-cache.js index ec6b052..7badc2e 100644 --- a/src/stores/editor-cache.js +++ b/src/stores/editor-cache.js @@ -4,6 +4,7 @@ import { NoteFormat } from "../common/note-format" import { useSettingsStore } from './settings-store' import { useErrorStore } from './error-store' +import { useHeynoteStore } from './heynote-store' import { HeynoteEditor } from '../editor/editor' const NUM_EDITOR_INSTANCES = 5 @@ -15,16 +16,17 @@ export const useEditorCacheStore = defineStore("editorCache", { cache: {}, watchHandler: null, themeWatchHandler: null, + containerElement: null, }, }), actions: { - createEditor(path, element) { + createEditor(path) { const settingsStore = useSettingsStore() const errorStore = useErrorStore() try { return new HeynoteEditor({ - element: element, + element: this.containerElement, path: path, theme: settingsStore.theme, keymap: settingsStore.settings.keymap, @@ -43,11 +45,29 @@ export const useEditorCacheStore = defineStore("editorCache", { } }, + getOrCreateEditor(path, updateLru) { + if (updateLru) { + // move to end of LRU + this.editorCache.lru = this.editorCache.lru.filter(p => p !== path) + this.editorCache.lru.push(path) + } + if (this.editorCache.cache[path]) { + return this.editorCache.cache[path] + } else { + const editor = this.createEditor(path) + this.addEditor(path, editor) + if (!updateLru) { + // if need to add the editor to the LRU, but at the top so that it is the first to be removed + this.editorCache.lru.unshift(path) + } + return editor + } + }, + getEditor(path) { // move to end of LRU this.editorCache.lru = this.editorCache.lru.filter(p => p !== path) this.editorCache.lru.push(path) - if (this.editorCache.cache[path]) { return this.editorCache.cache[path] } @@ -90,7 +110,8 @@ export const useEditorCacheStore = defineStore("editorCache", { }) }, - setUp() { + setUp(containerElement) { + this.containerElement = containerElement const settingsStore = useSettingsStore() this.watchHandler = watch(() => settingsStore.settings, (newSettings, oldSettings) => { //console.log("Settings changed (watch)", newSettings, oldSettings) @@ -144,5 +165,25 @@ export const useEditorCacheStore = defineStore("editorCache", { window.document.removeEventListener("currenciesLoaded", this.onCurrenciesLoaded) }, + + moveCurrentBlockToOtherEditor(targetPath) { + const heynoteStore = useHeynoteStore() + + const editor = toRaw(this.getEditor(heynoteStore.currentBufferPath)) + let otherEditor = toRaw(this.getOrCreateEditor(targetPath, false)) + otherEditor.hide() + + const content = editor.getActiveBlockContent() + otherEditor.contentLoadedPromise.then(() => { + otherEditor.appendBlockContent(content) + editor.deleteActiveBlock() + + // add the target buffer to recent buffers so that it shows up at the top of the buffer selector + heynoteStore.addRecentBuffer(targetPath) + heynoteStore.addRecentBuffer(heynoteStore.currentBufferPath) + }) + + //console.log("LRU", this.editorCache.lru) + } }, }) diff --git a/src/stores/heynote-store.js b/src/stores/heynote-store.js index 589d5fc..1c34ee4 100644 --- a/src/stores/heynote-store.js +++ b/src/stores/heynote-store.js @@ -27,6 +27,7 @@ export const useHeynoteStore = defineStore("heynote", { showLanguageSelector: false, showCreateBuffer: false, showEditBuffer: false, + showMoveToBufferSelector: false, }), actions: { @@ -41,7 +42,10 @@ export const useHeynoteStore = defineStore("heynote", { openBuffer(path) { this.closeDialog() this.currentBufferPath = path + this.addRecentBuffer(path) + }, + addRecentBuffer(path) { const recent = this.recentBufferPaths.filter((p) => p !== path) recent.unshift(path) this.recentBufferPaths = recent.slice(0, 100) @@ -55,6 +59,10 @@ export const useHeynoteStore = defineStore("heynote", { this.closeDialog() this.showBufferSelector = true }, + openMoveToBufferSelector() { + this.closeDialog() + this.showMoveToBufferSelector = true + }, openCreateBuffer(createMode, nameSuggestion) { createMode = createMode || "new" this.closeDialog() @@ -69,12 +77,17 @@ export const useHeynoteStore = defineStore("heynote", { this.showBufferSelector = false this.showLanguageSelector = false this.showEditBuffer = false + this.showMoveToBufferSelector = false }, closeBufferSelector() { this.showBufferSelector = false }, + closeMoveToBufferSelector() { + this.showMoveToBufferSelector = false + }, + editBufferMetadata(path) { if (this.currentBufferPath !== path) { this.openBuffer(path) diff --git a/tests/buffer-creation.spec.js b/tests/buffer-creation.spec.js index 708eb9e..3ba4134 100644 --- a/tests/buffer-creation.spec.js +++ b/tests/buffer-creation.spec.js @@ -3,7 +3,7 @@ import {HeynotePage} from "./test-utils.js"; import { AUTO_SAVE_INTERVAL } from "../src/common/constants.js" import { NoteFormat } from "../src/common/note-format.js" -import exp from "constants"; + let heynotePage @@ -40,11 +40,12 @@ test("default buffer saved", async ({page}) => { test("create new buffer from block", async ({page}) => { await page.locator("body").press(heynotePage.agnosticKey("Mod+S")) await page.waitForTimeout(50) + await page.locator("body").press("ArrowUp") + await page.locator("body").press("Enter") + await page.waitForTimeout(50) await page.locator("body").pressSequentially("My New Buffer") await page.locator("body").press("Enter") await page.waitForTimeout(150) - await page.locator("body").press("Enter") - await page.locator("body").pressSequentially("New buffer content") await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50); const buffers = Object.keys(await heynotePage.getStoredBufferList()) @@ -62,8 +63,7 @@ Block B`) expect(newBuffer.content).toBe(` ∞∞∞text -Block C -New buffer content`) +Block C`) }) diff --git a/tests/move-block.spec.js b/tests/move-block.spec.js new file mode 100644 index 0000000..ec909c8 --- /dev/null +++ b/tests/move-block.spec.js @@ -0,0 +1,101 @@ +import {expect, test} from "@playwright/test"; +import {HeynotePage} from "./test-utils.js"; + +import { AUTO_SAVE_INTERVAL } from "../src/common/constants.js" +import { NoteFormat } from "../src/common/note-format.js" + + +let heynotePage + +test.beforeEach(async ({page}) => { + heynotePage = new HeynotePage(page) + await heynotePage.goto() + + expect((await heynotePage.getBlocks()).length).toBe(1) + await heynotePage.setContent(` +∞∞∞text +Block A +∞∞∞text +Block B +∞∞∞text +Block C`) + await page.waitForTimeout(100); + // check that blocks are created + expect((await heynotePage.getBlocks()).length).toBe(3) + + // check that visual block layers are created + await expect(page.locator("css=.heynote-blocks-layer > div")).toHaveCount(3) +}); + + +test("move block to other buffer", async ({page}) => { + await heynotePage.saveBuffer("other.txt", ` +∞∞∞text-a +First block +∞∞∞math +Second block`) + await page.locator("body").press(heynotePage.agnosticKey("Mod+S")) + await page.waitForTimeout(50) + await page.locator("body").press("Enter") + await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50); + + const buffers = Object.keys(await heynotePage.getStoredBufferList()) + expect(buffers).toContain("other.txt") + + const otherBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("other.txt")) + + expect(await heynotePage.getContent()).toBe(` +∞∞∞text +Block A +∞∞∞text +Block B`) + + expect(otherBuffer.content).toBe(` +∞∞∞text-a +First block +∞∞∞math +Second block +∞∞∞text +Block C`) + +}) + + +test("move block to other open/cached buffer", async ({page}) => { + await heynotePage.saveBuffer("other.txt", ` +∞∞∞text-a +First block +∞∞∞math +Second block`) + await page.locator("body").press(heynotePage.agnosticKey("Mod+P")) + await page.locator("body").press("Enter") + await page.waitForTimeout(50) + await page.locator("body").press(heynotePage.agnosticKey("Mod+P")) + await page.locator("body").press("Enter") + await page.waitForTimeout(50) + await page.locator("body").press(heynotePage.agnosticKey("Mod+S")) + await page.waitForTimeout(50) + await page.locator("body").press("Enter") + await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50); + + const buffers = Object.keys(await heynotePage.getStoredBufferList()) + expect(buffers).toContain("other.txt") + + const otherBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("other.txt")) + + expect(await heynotePage.getContent()).toBe(` +∞∞∞text +Block A +∞∞∞text +Block B`) + + expect(otherBuffer.content).toBe(` +∞∞∞text-a +First block +∞∞∞math +Second block +∞∞∞text +Block C`) + +}) + diff --git a/tests/test-utils.js b/tests/test-utils.js index 209701a..f2b1a4c 100644 --- a/tests/test-utils.js +++ b/tests/test-utils.js @@ -38,7 +38,10 @@ export class HeynotePage { async setContent(content) { await expect(this.page.locator("css=.cm-editor")).toBeVisible() - await this.page.evaluate((content) => window._heynote_editor.setContent(content), content) + await this.page.evaluate(async (content) => { + await window._heynote_editor.setContent(content) + await window._heynote_editor.save() + }, content) } async getCursorPosition() { @@ -65,6 +68,12 @@ export class HeynotePage { return await this.page.evaluate((path) => window.heynote.buffer.load(path), path) } + async saveBuffer(path, content) { + const format = new NoteFormat() + format.content = content + await this.page.evaluate(({path, content}) => window.heynote.buffer.save(path, content), {path, content:format.serialize()}) + } + agnosticKey(key) { return key.replace("Mod", this.isMac ? "Meta" : "Control") }