From 80a6fdca698db092a9efa27d3c474afe5f0a33d6 Mon Sep 17 00:00:00 2001 From: mf Date: Fri, 19 Jun 2026 12:14:34 +0200 Subject: [PATCH] Move single list items with Alt Shift arrows (#143) Adds Alt+Shift+Up/Down for moving only the current list or task item while preserving the existing Alt+Up/Down block movement. Reviewed-on: https://codeberg.org/ArkHost/HelixNotes/pulls/143 --- src/lib/components/Editor.svelte | 43 +++++++++++++++++++++++++++-- src/lib/components/InfoPanel.svelte | 1 + 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index fbd58f8..d2b3c8a 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -1325,6 +1325,45 @@ event.preventDefault(); const { state, dispatch } = view; const resolvedPos = state.selection.$from; + if (event.shiftKey) { + for (let depth = resolvedPos.depth; depth > 0; depth--) { + const itemNode = resolvedPos.node(depth); + if (itemNode.type.name !== 'listItem' && itemNode.type.name !== 'taskItem') continue; + const parentListDepth = depth - 1; + const itemIndex = resolvedPos.index(parentListDepth); + const itemPos = resolvedPos.before(depth); + const itemSlice = state.doc.slice(itemPos, itemPos + itemNode.nodeSize); + const cursorOffset = resolvedPos.pos - itemPos; + const tr = state.tr; + + if (event.key === 'ArrowUp') { + if (itemIndex <= 0) return true; + const prevPos = resolvedPos.posAtIndex(itemIndex - 1, parentListDepth); + tr.delete(itemPos, itemPos + itemNode.nodeSize); + const insertAt = tr.mapping.map(prevPos); + tr.insert(insertAt, itemSlice.content); + const newCursorPos = Math.min(insertAt + cursorOffset, tr.doc.content.size); + tr.setSelection(Selection.near(tr.doc.resolve(newCursorPos))); + dispatch(tr.scrollIntoView()); + return true; + } + + const parentList = resolvedPos.node(parentListDepth); + if (itemIndex >= parentList.childCount - 1) return true; + const nextPos = resolvedPos.posAtIndex(itemIndex + 1, parentListDepth); + const nextNode = state.doc.nodeAt(nextPos); + if (!nextNode) return true; + const nextSlice = state.doc.slice(nextPos, nextPos + nextNode.nodeSize); + tr.delete(nextPos, nextPos + nextNode.nodeSize); + const insertAt = tr.mapping.map(itemPos); + tr.insert(insertAt, nextSlice.content); + const newCursorPos = Math.min(tr.mapping.map(itemPos) + cursorOffset, tr.doc.content.size); + tr.setSelection(Selection.near(tr.doc.resolve(newCursorPos))); + dispatch(tr.scrollIntoView()); + return true; + } + return true; + } // Find the top-level block index const depth = 1; // top-level blocks in doc if (resolvedPos.depth < depth) return true; @@ -1345,7 +1384,7 @@ const insertAt = tr.mapping.map(prevPos); tr.insert(insertAt, curSlice.content); const newCursorPos = Math.min(insertAt + cursorOffset, tr.doc.content.size); - tr.setSelection(state.selection.constructor.near(tr.doc.resolve(newCursorPos))); + tr.setSelection(Selection.near(tr.doc.resolve(newCursorPos))); dispatch(tr.scrollIntoView()); } else { if (parentIndex >= state.doc.childCount - 1) return true; @@ -1360,7 +1399,7 @@ const insertAt = tr.mapping.map(parentPos); tr.insert(insertAt, nextSlice.content); const newCursorPos = Math.min(tr.mapping.map(parentPos) + cursorOffset, tr.doc.content.size); - tr.setSelection(state.selection.constructor.near(tr.doc.resolve(newCursorPos))); + tr.setSelection(Selection.near(tr.doc.resolve(newCursorPos))); dispatch(tr.scrollIntoView()); } return true; diff --git a/src/lib/components/InfoPanel.svelte b/src/lib/components/InfoPanel.svelte index 6c3e16b..890ace9 100644 --- a/src/lib/components/InfoPanel.svelte +++ b/src/lib/components/InfoPanel.svelte @@ -189,6 +189,7 @@
Undo{modKey}+Z
Redo{modKey}+Shift+Z
Move line up / downAlt+↑↓
+
Move list item up / downAlt+Shift+↑↓

Editor Commands

Slash commands/