From f2b3524d87146b5f107cf054f06c51a9e4342fe7 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 16 Apr 2023 09:49:19 -0400 Subject: [PATCH] fix: Ctrl-a/e in code fences --- .../editor/commands/backspaceToParagraph.ts | 7 + shared/editor/commands/codeFence.ts | 122 ++++++++++++++++++ shared/editor/nodes/CodeFence.ts | 53 ++------ 3 files changed, 141 insertions(+), 41 deletions(-) create mode 100644 shared/editor/commands/codeFence.ts diff --git a/shared/editor/commands/backspaceToParagraph.ts b/shared/editor/commands/backspaceToParagraph.ts index be9bf9e41..e4ad0f0c8 100644 --- a/shared/editor/commands/backspaceToParagraph.ts +++ b/shared/editor/commands/backspaceToParagraph.ts @@ -2,6 +2,13 @@ import { NodeType } from "prosemirror-model"; import { EditorState } from "prosemirror-state"; import { Dispatch } from "../types"; +/** + * Converts the current node to a paragraph when pressing backspace at the + * beginning of the node and not already a paragraph. + * + * @param type The node type + * @returns A prosemirror command. + */ export default function backspaceToParagraph(type: NodeType) { return (state: EditorState, dispatch: Dispatch) => { const { $from, from, to, empty } = state.selection; diff --git a/shared/editor/commands/codeFence.ts b/shared/editor/commands/codeFence.ts new file mode 100644 index 000000000..a31478edd --- /dev/null +++ b/shared/editor/commands/codeFence.ts @@ -0,0 +1,122 @@ +import { EditorState, TextSelection } from "prosemirror-state"; +import isInCode from "../queries/isInCode"; +import { Dispatch } from "../types"; + +/** + * Moves the current selection to the previous newline, this is used inside + * code fences only, prosemirror handles this functionality fine in other nodes. + * + * @returns A prosemirror command. + */ +export const moveToPreviousNewline = ( + state: EditorState, + dispatch: Dispatch +) => { + if (!isInCode(state)) { + return false; + } + + const $pos = state.selection.$from; + if (!$pos.parent.type.isTextblock) { + return false; + } + + // The easiest way to find the previous newline is to reverse the string and + // perform a forward seach as if looking for the next newline. + const beginningOfNode = $pos.pos - $pos.parentOffset; + const startOfLine = $pos.parent.textContent + .split("") + .reverse() + .join("") + .indexOf("\n", $pos.parent.nodeSize - $pos.parentOffset - 2); + + if (startOfLine === -1) { + return false; + } + + dispatch( + state.tr.setSelection( + TextSelection.create( + state.doc, + beginningOfNode + ($pos.parent.nodeSize - startOfLine - 2) + ) + ) + ); + + return true; +}; + +/** + * Moves the current selection to the next newline, this is used inside code + * fences only, prosemirror handles this functionality fine in other nodes. + * + * @returns A prosemirror command. + */ +export const moveToNextNewline = (state: EditorState, dispatch: Dispatch) => { + if (!isInCode(state)) { + return false; + } + + const $pos = state.selection.$to; + if (!$pos.parent.type.isTextblock) { + return false; + } + + // find next newline + const beginningOfNode = $pos.pos - $pos.parentOffset; + const endOfLine = $pos.parent.textContent.indexOf("\n", $pos.parentOffset); + if (endOfLine === -1) { + return false; + } + + dispatch( + state.tr.setSelection( + TextSelection.create(state.doc, beginningOfNode + endOfLine) + ) + ); + + return true; +}; + +/** + * Replace the selection with a newline character preceeded by a number of + * spaces to have the new line align with the code on the previous. This is + * standard code editor behavior. + * + * @returns A prosemirror command + */ +export const newlineInCode = (state: EditorState, dispatch: Dispatch) => { + if (!isInCode(state)) { + return false; + } + const { tr, selection } = state; + const text = selection?.$anchor?.nodeBefore?.text; + + let newText = "\n"; + + if (text) { + const splitByNewLine = text.split("\n"); + const numOfSpaces = splitByNewLine[splitByNewLine.length - 1].search( + /\S|$/ + ); + newText += " ".repeat(numOfSpaces); + } + + dispatch(tr.insertText(newText, selection.from, selection.to)); + return true; +}; + +/** + * Insert two spaces to simulate the tab key. + * + * @returns A prosemirror command + */ +export const insertSpaceTab = (state: EditorState, dispatch: Dispatch) => { + if (!isInCode(state)) { + return false; + } + + const { tr, selection } = state; + dispatch(tr.insertText(" ", selection.from, selection.to)); + return true; +}; diff --git a/shared/editor/nodes/CodeFence.ts b/shared/editor/nodes/CodeFence.ts index f865dd57e..f4926aa78 100644 --- a/shared/editor/nodes/CodeFence.ts +++ b/shared/editor/nodes/CodeFence.ts @@ -7,14 +7,7 @@ import { Schema, Node as ProsemirrorNode, } from "prosemirror-model"; -import { - EditorState, - Selection, - TextSelection, - Transaction, - Plugin, - PluginKey, -} from "prosemirror-state"; +import { Selection, Plugin, PluginKey } from "prosemirror-state"; import refractor from "refractor/core"; import bash from "refractor/lang/bash"; import clike from "refractor/lang/clike"; @@ -56,12 +49,17 @@ import zig from "refractor/lang/zig"; import { Dictionary } from "~/hooks/useDictionary"; import { UserPreferences } from "../../types"; import Storage from "../../utils/Storage"; +import { + newlineInCode, + insertSpaceTab, + moveToNextNewline, + moveToPreviousNewline, +} from "../commands/codeFence"; import toggleBlockType from "../commands/toggleBlockType"; import Mermaid from "../extensions/Mermaid"; import Prism, { LANGUAGES } from "../extensions/Prism"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; import isInCode from "../queries/isInCode"; -import { Dispatch } from "../types"; import Node from "./Node"; const PERSISTENCE_KEY = "rme-code-language"; @@ -228,38 +226,11 @@ export default class CodeFence extends Node { keys({ type, schema }: { type: NodeType; schema: Schema }) { return { "Shift-Ctrl-\\": toggleBlockType(type, schema.nodes.paragraph), - "Shift-Enter": (state: EditorState, dispatch: Dispatch) => { - if (!isInCode(state)) { - return false; - } - const { - tr, - selection, - }: { tr: Transaction; selection: TextSelection } = state; - const text = selection?.$anchor?.nodeBefore?.text; - - let newText = "\n"; - - if (text) { - const splitByNewLine = text.split("\n"); - const numOfSpaces = splitByNewLine[splitByNewLine.length - 1].search( - /\S|$/ - ); - newText += " ".repeat(numOfSpaces); - } - - dispatch(tr.insertText(newText, selection.from, selection.to)); - return true; - }, - Tab: (state: EditorState, dispatch: Dispatch) => { - if (!isInCode(state)) { - return false; - } - - const { tr, selection } = state; - dispatch(tr.insertText(" ", selection.from, selection.to)); - return true; - }, + Tab: insertSpaceTab, + Enter: newlineInCode, + "Shift-Enter": newlineInCode, + "Ctrl-a": moveToPreviousNewline, + "Ctrl-e": moveToNextNewline, }; }