From 32ef51161d9c55283cbf829793089a62ef83f478 Mon Sep 17 00:00:00 2001 From: Yuri Karamian Date: Fri, 7 Aug 2026 21:50:34 +0200 Subject: [PATCH] Scroll code block into view after fence input rule --- src/lib/components/Editor.svelte | 2 + .../editor/extensions/codeBlockInputScroll.ts | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 src/lib/editor/extensions/codeBlockInputScroll.ts diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index cc5d1a7..26337e4 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -48,6 +48,7 @@ import { debounce } from '$lib/utils/debounce'; import { encryptSecretText, decryptSecretText, readSecretTitle } from '$lib/utils/secrets'; import { WrapSelectedText } from '$lib/editor/extensions/wrapSelectedText'; + import { CodeBlockInputScroll } from '$lib/editor/extensions/codeBlockInputScroll'; import { calloutGroup, calloutIcon, calloutLabel, CALLOUT_MENU, transformCalloutBlockquotes, serializeCallout } from '$lib/editor/callouts'; import { wrapTextareaSelection } from '$lib/editor/source/selectionPairs'; import { convertListNode, type MixedListName } from '$lib/editor/mixedLists'; @@ -4396,6 +4397,7 @@ TextStyle, Color, CodeBlockLowlight.configure({ lowlight, enableTabIndentation: true, defaultLanguage: 'text' }), + CodeBlockInputScroll, CodeBlockLanguageSelect, CopyButtonExtension, MermaidRenderer, diff --git a/src/lib/editor/extensions/codeBlockInputScroll.ts b/src/lib/editor/extensions/codeBlockInputScroll.ts new file mode 100644 index 0000000..bc8bf69 --- /dev/null +++ b/src/lib/editor/extensions/codeBlockInputScroll.ts @@ -0,0 +1,49 @@ +import { Extension } from '@tiptap/core'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; + +const CODE_FENCE_PATTERN = /^(?:```|~~~)(?:[a-z]+)?$/; + +function isInputRulesPlugin(plugin: Plugin) { + return 'isInputRules' in plugin.spec && plugin.spec.isInputRules === true; +} + +export function createCodeBlockInputScrollPlugin() { + return new Plugin({ + key: new PluginKey('codeBlockInputScroll'), + appendTransaction(transactions, oldState, newState) { + const inputRulesPlugin = oldState.plugins.find(isInputRulesPlugin); + const wasKeyboardInputRule = + inputRulesPlugin !== undefined && + transactions.some((transaction) => { + const metadata: unknown = transaction.getMeta(inputRulesPlugin); + + return ( + typeof metadata === 'object' && + metadata !== null && + 'transform' in metadata && + metadata.transform === transaction && + 'text' in metadata && + metadata.text === '\n' + ); + }); + const { selection } = oldState; + const wasFenceParagraph = + selection.empty && + selection.$from.parent.type.name === 'paragraph' && + CODE_FENCE_PATTERN.test(selection.$from.parent.textContent); + const isCodeBlock = newState.selection.$from.parent.type.name === 'codeBlock'; + + return wasKeyboardInputRule && wasFenceParagraph && isCodeBlock + ? newState.tr.scrollIntoView() + : null; + }, + }); +} + +export const CodeBlockInputScroll = Extension.create({ + name: 'codeBlockInputScroll', + + addProseMirrorPlugins() { + return [createCodeBlockInputScrollPlugin()]; + }, +});