From 81bbc2ed47a357ed467be5cd777e28875afd714f Mon Sep 17 00:00:00 2001 From: wolimst <64784258+wolimst@users.noreply.github.com> Date: Tue, 20 Feb 2024 23:25:27 +0900 Subject: [PATCH 1/5] Add feature for moving the current block up and down --- src/editor/block/commands.js | 55 ++++++++++++++++++++++++++++++++++++ src/editor/keymap.js | 3 ++ 2 files changed, 58 insertions(+) diff --git a/src/editor/block/commands.js b/src/editor/block/commands.js index 7265b37..63144f7 100644 --- a/src/editor/block/commands.js +++ b/src/editor/block/commands.js @@ -313,3 +313,58 @@ export function triggerCurrenciesLoaded(state, dispatch) { annotations: [heynoteEvent.of(CURRENCIES_LOADED)], })) } + +export function moveCurrentBlockUp({state, dispatch}) { + return moveCurrentBlock(state, dispatch, true) +} + +export function moveCurrentBlockDown({state, dispatch}) { + return moveCurrentBlock(state, dispatch, false) +} + +function moveCurrentBlock(state, dispatch, up) { + if (state.readOnly) { + return false + } + + const blocks = state.facet(blockState) + const currentBlock = getActiveNoteBlock(state) + const blockIndex = blocks.indexOf(currentBlock) + if ((up && blockIndex === 0) || (!up && blockIndex === blocks.length - 1)) { + return false + } + + const dir = up ? -1 : 1 + const neighborBlock = blocks[blockIndex + dir] + + const currentBlockContent = state.sliceDoc(currentBlock.delimiter.from, currentBlock.content.to) + const neighborBlockContent = state.sliceDoc(neighborBlock.delimiter.from, neighborBlock.content.to) + const newContent = up ? currentBlockContent + neighborBlockContent : neighborBlockContent + currentBlockContent + + const selectionRange = state.selection.asSingle().ranges[0] + let newSelectionRange + if (up) { + newSelectionRange = EditorSelection.range( + selectionRange.anchor - currentBlock.delimiter.from + neighborBlock.delimiter.from, + selectionRange.head - currentBlock.delimiter.from + neighborBlock.delimiter.from, + ) + } else { + newSelectionRange = EditorSelection.range( + selectionRange.anchor + neighborBlock.content.to - neighborBlock.delimiter.from, + selectionRange.head + neighborBlock.content.to - neighborBlock.delimiter.from, + ) + } + + dispatch(state.update({ + changes: { + from: up ? neighborBlock.delimiter.from : currentBlock.delimiter.from, + to: up ? currentBlock.content.to : neighborBlock.content.to, + insert: newContent, + }, + selection: newSelectionRange, + }, { + scrollIntoView: true, + userEvent: "input", + })) + return true +} diff --git a/src/editor/keymap.js b/src/editor/keymap.js index 745aaf3..1037bb9 100644 --- a/src/editor/keymap.js +++ b/src/editor/keymap.js @@ -15,6 +15,7 @@ import { gotoPreviousParagraph, gotoNextParagraph, selectNextParagraph, selectPreviousParagraph, newCursorBelow, newCursorAbove, + moveCurrentBlockUp, moveCurrentBlockDown, } from "./block/commands.js" import { pasteCommand, copyCommand, cutCommand } from "./copy-paste.js" @@ -65,5 +66,7 @@ export function heynoteKeymap(editor) { {key:"Mod-ArrowDown", run:gotoNextBlock, shift:selectNextBlock}, {key:"Ctrl-ArrowUp", run:gotoPreviousParagraph, shift:selectPreviousParagraph}, {key:"Ctrl-ArrowDown", run:gotoNextParagraph, shift:selectNextParagraph}, + ["Mod-Shift-Alt-ArrowUp", moveCurrentBlockUp], + ["Mod-Shift-Alt-ArrowDown", moveCurrentBlockDown], ]) } From 650bb18c0b25928a1415c4410abf4aaa23ad36c9 Mon Sep 17 00:00:00 2001 From: wolimst <64784258+wolimst@users.noreply.github.com> Date: Thu, 22 Feb 2024 21:11:30 +0900 Subject: [PATCH 2/5] Add annotation on state change to ignore first block protection filter --- src/editor/annotation.js | 1 + src/editor/block/commands.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/editor/annotation.js b/src/editor/annotation.js index 6b4e83c..ad0c5eb 100644 --- a/src/editor/annotation.js +++ b/src/editor/annotation.js @@ -5,3 +5,4 @@ export const LANGUAGE_CHANGE = "heynote-change" export const CURRENCIES_LOADED = "heynote-currencies-loaded" export const SET_CONTENT = "heynote-set-content" export const ADD_NEW_BLOCK = "heynote-add-new-block" +export const MOVE_BLOCK = "heynote-move-block" diff --git a/src/editor/block/commands.js b/src/editor/block/commands.js index 63144f7..08c9ff9 100644 --- a/src/editor/block/commands.js +++ b/src/editor/block/commands.js @@ -1,5 +1,5 @@ import { EditorSelection } from "@codemirror/state" -import { heynoteEvent, LANGUAGE_CHANGE, CURRENCIES_LOADED, ADD_NEW_BLOCK } from "../annotation.js"; +import { heynoteEvent, LANGUAGE_CHANGE, CURRENCIES_LOADED, ADD_NEW_BLOCK, MOVE_BLOCK } from "../annotation.js"; import { blockState, getActiveNoteBlock, getFirstNoteBlock, getLastNoteBlock, getNoteBlockFromPos } from "./block" import { moveLineDown, moveLineUp } from "./move-lines.js"; import { selectAll } from "./select-all.js"; @@ -362,6 +362,7 @@ function moveCurrentBlock(state, dispatch, up) { insert: newContent, }, selection: newSelectionRange, + annotations: [heynoteEvent.of(MOVE_BLOCK)], }, { scrollIntoView: true, userEvent: "input", From 4e1ddac12ca80dce544e3da25c7bc4d43fdf6c21 Mon Sep 17 00:00:00 2001 From: wolimst <64784258+wolimst@users.noreply.github.com> Date: Sun, 25 Feb 2024 15:31:41 +0900 Subject: [PATCH 3/5] Add tests for block moving feature --- tests/move-block.spec.js | 112 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 tests/move-block.spec.js diff --git a/tests/move-block.spec.js b/tests/move-block.spec.js new file mode 100644 index 0000000..56206ab --- /dev/null +++ b/tests/move-block.spec.js @@ -0,0 +1,112 @@ +import { expect, test } from "@playwright/test" +import { HeynotePage } from "./test-utils.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`) + + // 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 the first block up", async ({ page }) => { + // select the first block, cursor position: "Block A|" + await page.locator("body").press("ArrowUp") + await page.locator("body").press("ArrowUp") + + await page.locator("body").press(`${heynotePage.isMac ? "Meta" : "Control"}+Shift+Alt+ArrowUp`) + const cursorPosition = await heynotePage.getCursorPosition() + const content = await heynotePage.getContent() + + expect((await heynotePage.getBlocks()).length).toBe(3) + expect(await heynotePage.getBlockContent(0)).toBe("Block A") + expect(await heynotePage.getBlockContent(1)).toBe("Block B") + expect(await heynotePage.getBlockContent(2)).toBe("Block C") + expect(content.slice(cursorPosition - 1, cursorPosition)).toBe("A") +}) + +test("move the middle block up", async ({ page }) => { + // select the second block, cursor position: "Block B|" + await page.locator("body").press("ArrowUp") + + await page.locator("body").press(`${heynotePage.isMac ? "Meta" : "Control"}+Shift+Alt+ArrowUp`) + const cursorPosition = await heynotePage.getCursorPosition() + const content = await heynotePage.getContent() + + expect((await heynotePage.getBlocks()).length).toBe(3) + expect(await heynotePage.getBlockContent(0)).toBe("Block B") + expect(await heynotePage.getBlockContent(1)).toBe("Block A") + expect(await heynotePage.getBlockContent(2)).toBe("Block C") + expect(content.slice(cursorPosition - 1, cursorPosition)).toBe("B") +}) + +test("move the last block up", async ({ page }) => { + // cursor position: "Block C|" + await page.locator("body").press(`${heynotePage.isMac ? "Meta" : "Control"}+Shift+Alt+ArrowUp`) + const cursorPosition = await heynotePage.getCursorPosition() + const content = await heynotePage.getContent() + + expect((await heynotePage.getBlocks()).length).toBe(3) + expect(await heynotePage.getBlockContent(0)).toBe("Block A") + expect(await heynotePage.getBlockContent(1)).toBe("Block C") + expect(await heynotePage.getBlockContent(2)).toBe("Block B") + expect(content.slice(cursorPosition - 1, cursorPosition)).toBe("C") +}) + +test("move the first block down", async ({ page }) => { + // select the first block, cursor position: "Block A|" + await page.locator("body").press("ArrowUp") + await page.locator("body").press("ArrowUp") + + await page.locator("body").press(`${heynotePage.isMac ? "Meta" : "Control"}+Shift+Alt+ArrowDown`) + const cursorPosition = await heynotePage.getCursorPosition() + const content = await heynotePage.getContent() + + expect((await heynotePage.getBlocks()).length).toBe(3) + expect(await heynotePage.getBlockContent(0)).toBe("Block B") + expect(await heynotePage.getBlockContent(1)).toBe("Block A") + expect(await heynotePage.getBlockContent(2)).toBe("Block C") + expect(content.slice(cursorPosition - 1, cursorPosition)).toBe("A") +}) + +test("move the middle block down", async ({ page }) => { + // select the second block, cursor position: "Block B|" + await page.locator("body").press("ArrowUp") + + await page.locator("body").press(`${heynotePage.isMac ? "Meta" : "Control"}+Shift+Alt+ArrowDown`) + const cursorPosition = await heynotePage.getCursorPosition() + const content = await heynotePage.getContent() + + expect((await heynotePage.getBlocks()).length).toBe(3) + expect(await heynotePage.getBlockContent(0)).toBe("Block A") + expect(await heynotePage.getBlockContent(1)).toBe("Block C") + expect(await heynotePage.getBlockContent(2)).toBe("Block B") + expect(content.slice(cursorPosition - 1, cursorPosition)).toBe("B") +}) + +test("move the last block down", async ({ page }) => { + // cursor position: "Block C|" + await page.locator("body").press(`${heynotePage.isMac ? "Meta" : "Control"}+Shift+Alt+ArrowDown`) + const cursorPosition = await heynotePage.getCursorPosition() + const content = await heynotePage.getContent() + + expect((await heynotePage.getBlocks()).length).toBe(3) + expect(await heynotePage.getBlockContent(0)).toBe("Block A") + expect(await heynotePage.getBlockContent(1)).toBe("Block B") + expect(await heynotePage.getBlockContent(2)).toBe("Block C") + expect(content.slice(cursorPosition - 1, cursorPosition)).toBe("C") +}) From b9be7bc9fb603e64e12eba98007ebda43c797c0d Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 22 Apr 2025 18:02:45 +0200 Subject: [PATCH 4/5] Add move block commands for new keybinding system --- src/editor/commands.js | 3 +++ src/editor/keymap.js | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/editor/commands.js b/src/editor/commands.js index 8842ae6..3693f27 100644 --- a/src/editor/commands.js +++ b/src/editor/commands.js @@ -22,6 +22,7 @@ import { selectAll, deleteBlock, deleteBlockSetCursorPreviousBlock, newCursorBelow, newCursorAbove, + moveCurrentBlockUp, moveCurrentBlockDown, } from "./block/commands.js" import { deleteLine } from "./block/delete-line.js" import { formatBlockContent } from "./block/format-code.js" @@ -85,6 +86,8 @@ const HEYNOTE_COMMANDS = { insertNewBlockAtCursor: cmd(insertNewBlockAtCursor, "Block", "Insert new block at cursor"), deleteBlock: cmd(deleteBlock, "Block", "Delete block"), deleteBlockSetCursorPreviousBlock: cmd(deleteBlockSetCursorPreviousBlock, "Block", "Delete block and set cursor to previous block"), + moveCurrentBlockUp: cmdLessContext(moveCurrentBlockUp, "Block", "Move current block up"), + moveCurrentBlockDown: cmdLessContext(moveCurrentBlockDown, "Block", "Move current block down"), cursorPreviousBlock: cmd(cursorPreviousBlock, "Cursor", "Move cursor to previous block"), cursorNextBlock: cmd(cursorNextBlock, "Cursor", "Move cursor to next block"), cursorPreviousParagraph: cmd(cursorPreviousParagraph, "Cursor", "Move cursor to previous paragraph"), diff --git a/src/editor/keymap.js b/src/editor/keymap.js index 233d616..a396dc9 100644 --- a/src/editor/keymap.js +++ b/src/editor/keymap.js @@ -40,6 +40,8 @@ export const DEFAULT_KEYMAP = [ ...cmdShift("PageDown", "cursorPageDown", "selectPageDown"), ...cmdShift("Home", "cursorLineBoundaryBackward", "selectLineBoundaryBackward"), ...cmdShift("End", "cursorLineBoundaryForward", "selectLineBoundaryForward"), + cmd("Alt-Mod-Shift-ArrowUp", "moveCurrentBlockUp"), + cmd("Alt-Mod-Shift-ArrowDown", "moveCurrentBlockDown"), cmd("Backspace", "deleteCharBackward"), cmd("Delete", "deleteCharForward"), cmd("Escape", "simplifySelection"), From cdc41b08d6ca7faa1603c3c0eddb6af649630c26 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 22 Apr 2025 18:05:36 +0200 Subject: [PATCH 5/5] Add Changelog entry about move blocks up/down function --- docs/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.md b/docs/changelog.md index 090c4fa..a6bad58 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -9,6 +9,7 @@ Here are the most notable changes in each release. For a more detailed list of c - Added support for custom key bindings. See [the documentation](https://heynote.com/docs/#user-content-custom-key-bindings) for more info. - Added a "command palette" that can be accessed by pressing `Ctrl/Cmd+Shift+P`, or just typing `>` in the buffer selector. The command palette allows you to discover all available commands in the app, and to quickly execute them. - Added support for configuring the tab size. +- Added functionality for moving blocks up and down. Default key bindings are `Ctrl/Cmd+Alt+Shift+Up` and `Ctrl/Cmd+Alt+Shift+Down`. ### Other changes