Merge pull request #204 from wolimst/feat/move-block

Add feature for moving the current block up and down
This commit is contained in:
Jonatan Heyman 2025-04-22 18:13:47 +02:00 committed by GitHub
commit 74558769e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 270 additions and 96 deletions

View File

@ -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

View File

@ -5,6 +5,7 @@ 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"
export const DELETE_BLOCK = "heynote-delete-block"
export const CURSOR_CHANGE = "heynote-cursor-change"
export const APPEND_BLOCK = "heynote-append-block"

View File

@ -1,6 +1,6 @@
import { EditorSelection, Transaction } from "@codemirror/state"
import { heynoteEvent, LANGUAGE_CHANGE, CURRENCIES_LOADED, ADD_NEW_BLOCK, DELETE_BLOCK } from "../annotation.js";
import { heynoteEvent, LANGUAGE_CHANGE, CURRENCIES_LOADED, ADD_NEW_BLOCK, MOVE_BLOCK, DELETE_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";
@ -322,6 +322,62 @@ export function triggerCurrenciesLoaded(state, dispatch) {
}))
}
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,
annotations: [heynoteEvent.of(MOVE_BLOCK)],
}, {
scrollIntoView: true,
userEvent: "input",
}))
return true
}
export const deleteBlock = (editor) => ({state, dispatch}) => {
const range = state.selection.asSingle().ranges[0]
const blocks = state.facet(blockState)

View File

@ -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"),

View File

@ -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"),

View File

@ -0,0 +1,125 @@
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)
// create secondary buffer
await heynotePage.saveBuffer("other.txt", `
text-a
First block
math
Second block`)
});
test("move block to other buffer", async ({page}) => {
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 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`)
})
test("cursor position after moving first block", async ({page}) => {
await heynotePage.setCursorPosition(10)
expect(await heynotePage.getCursorPosition()).toBe(10)
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
await page.waitForTimeout(50)
await page.locator("body").press("Enter")
await page.waitForTimeout(50)
expect(await heynotePage.getCursorPosition()).toBe(9)
})
test("cursor position after moving middle block", async ({page}) => {
await heynotePage.setCursorPosition(28)
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
await page.waitForTimeout(50)
await page.locator("body").press("Enter")
await page.waitForTimeout(50)
expect(await heynotePage.getCursorPosition()).toBe(25)
})
test("cursor position after moving last block", async ({page}) => {
await heynotePage.setCursorPosition(48)
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
await page.waitForTimeout(50)
await page.locator("body").press("Enter")
await page.waitForTimeout(50)
expect(await heynotePage.getCursorPosition()).toBe(32)
})

View File

@ -1,9 +1,5 @@
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"
import { expect, test } from "@playwright/test"
import { HeynotePage } from "./test-utils.js"
let heynotePage
@ -19,107 +15,97 @@ Block A
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)
// create secondary buffer
await heynotePage.saveBuffer("other.txt", `
text-a
First block
math
Second block`)
});
test("move block to other buffer", async ({page}) => {
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 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")
test("move block to other open/cached buffer", async ({page}) => {
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`)
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("cursor position after moving first block", async ({page}) => {
await heynotePage.setCursorPosition(10)
expect(await heynotePage.getCursorPosition()).toBe(10)
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
await page.waitForTimeout(50)
await page.locator("body").press("Enter")
await page.waitForTimeout(50)
expect(await heynotePage.getCursorPosition()).toBe(9)
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("cursor position after moving middle block", async ({page}) => {
await heynotePage.setCursorPosition(28)
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
await page.waitForTimeout(50)
await page.locator("body").press("Enter")
await page.waitForTimeout(50)
expect(await heynotePage.getCursorPosition()).toBe(25)
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("cursor position after moving last block", async ({page}) => {
await heynotePage.setCursorPosition(48)
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
await page.waitForTimeout(50)
await page.locator("body").press("Enter")
await page.waitForTimeout(50)
expect(await heynotePage.getCursorPosition()).toBe(32)
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")
})