diff --git a/packages/editor/core/src/lib/editor-commands.ts b/packages/editor/core/src/lib/editor-commands.ts index 911347e7f..6e05ff13d 100644 --- a/packages/editor/core/src/lib/editor-commands.ts +++ b/packages/editor/core/src/lib/editor-commands.ts @@ -3,6 +3,7 @@ import { startImageUpload } from "src/ui/plugins/image/image-upload-handler"; import { findTableAncestor } from "src/lib/utils"; import { Selection } from "@tiptap/pm/state"; import { UploadImage } from "src/types/upload-image"; +import { replaceCodeWithText } from "src/ui/extensions/code/utils/replace-code-block-with-text"; export const setText = (editor: Editor, range?: Range) => { if (range) editor.chain().focus().deleteRange(range).clearNodes().run(); @@ -54,69 +55,11 @@ export const toggleUnderline = (editor: Editor, range?: Range) => { else editor.chain().focus().toggleUnderline().run(); }; -const replaceCodeBlockWithContent = (editor: Editor) => { - try { - const { schema } = editor.state; - const { paragraph } = schema.nodes; - let replaced = false; - - const replaceCodeBlock = (from: number, to: number, textContent: string) => { - const docSize = editor.state.doc.content.size; - - if (from < 0 || to > docSize || from > to) { - console.error("Invalid range for replacement: ", from, to, "in a document of size", docSize); - return; - } - - // split the textContent by new lines to handle each line as a separate paragraph - const lines = textContent.split(/\r?\n/); - - const tr = editor.state.tr; - - // Calculate the position for inserting the first paragraph - let insertPos = from; - - // Remove the code block first - tr.delete(from, to); - - // For each line, create a paragraph node and insert it - lines.forEach((line) => { - const paragraphNode = paragraph.create({}, schema.text(line)); - tr.insert(insertPos, paragraphNode); - // Update insertPos for the next insertion - insertPos += paragraphNode.nodeSize; - }); - - // Dispatch the transaction - editor.view.dispatch(tr); - replaced = true; - }; - - editor.state.doc.nodesBetween(editor.state.selection.from, editor.state.selection.to, (node, pos) => { - if (node.type === schema.nodes.codeBlock) { - const startPos = pos; - const endPos = pos + node.nodeSize; - const textContent = node.textContent; - if (textContent.length === 0) { - editor.chain().focus().toggleCodeBlock().run(); - } - replaceCodeBlock(startPos, endPos, textContent); - return false; - } - }); - - if (!replaced) { - console.log("No code block to replace."); - } - } catch (error) { - console.error("An error occurred while replacing code block content:", error); - } -}; - export const toggleCodeBlock = (editor: Editor, range?: Range) => { try { + // if it's a code block, replace it with the code with paragraphs if (editor.isActive("codeBlock")) { - replaceCodeBlockWithContent(editor); + replaceCodeWithText(editor); return; } @@ -124,11 +67,16 @@ export const toggleCodeBlock = (editor: Editor, range?: Range) => { const text = editor.state.doc.textBetween(from, to, "\n"); const isMultiline = text.includes("\n"); + // if the selection is not a range i.e. empty, then simply convert it into a code block if (editor.state.selection.empty) { editor.chain().focus().toggleCodeBlock().run(); } else if (isMultiline) { + // if the selection is multiline, then also replace the text content with + // a code block editor.chain().focus().deleteRange({ from, to }).insertContentAt(from, `\`\`\`\n${text}\n\`\`\``).run(); } else { + // if the selection is single line, then simply convert it into inline + // code editor.chain().focus().toggleCode().run(); } } catch (error) { diff --git a/packages/editor/core/src/ui/extensions/code-inline/index.tsx b/packages/editor/core/src/ui/extensions/code-inline/index.tsx index bc629160a..60a12364e 100644 --- a/packages/editor/core/src/ui/extensions/code-inline/index.tsx +++ b/packages/editor/core/src/ui/extensions/code-inline/index.tsx @@ -33,7 +33,7 @@ export const CustomCodeInlineExtension = Mark.create({ return { HTMLAttributes: { class: - "rounded bg-custom-background-80 px-1 py-[2px] font-mono font-medium text-orange-500 border-[0.5px] border-custom-border-200 text-sm", + "rounded bg-custom-background-80 px-1 py-[2px] font-mono font-medium text-orange-500 border-[0.5px] border-custom-border-200", spellcheck: "false", }, }; diff --git a/packages/editor/core/src/ui/extensions/code/utils/replace-code-block-with-text.ts b/packages/editor/core/src/ui/extensions/code/utils/replace-code-block-with-text.ts new file mode 100644 index 000000000..daf2c5f05 --- /dev/null +++ b/packages/editor/core/src/ui/extensions/code/utils/replace-code-block-with-text.ts @@ -0,0 +1,124 @@ +import { Editor, findParentNode } from "@tiptap/core"; + +type ReplaceCodeBlockParams = { + editor: Editor; + from: number; + to: number; + textContent: string; + cursorPosInsideCodeblock: number; +}; + +export function replaceCodeWithText(editor: Editor): void { + try { + const { from, to } = editor.state.selection; + const cursorPosInsideCodeblock = from; + let replaced = false; + + editor.state.doc.nodesBetween(from, to, (node, pos) => { + if (node.type === editor.state.schema.nodes.codeBlock) { + const startPos = pos; + const endPos = pos + node.nodeSize; + const textContent = node.textContent; + + if (textContent.length === 0) { + editor.chain().focus().toggleCodeBlock().run(); + } else { + transformCodeBlockToParagraphs({ + editor, + from: startPos, + to: endPos, + textContent, + cursorPosInsideCodeblock, + }); + } + + replaced = true; + return false; + } + }); + + if (!replaced) { + console.log("No code block to replace."); + } + } catch (error) { + console.error("An error occurred while replacing code block content:", error); + } +} + +function transformCodeBlockToParagraphs({ + editor, + from, + to, + textContent, + cursorPosInsideCodeblock, +}: ReplaceCodeBlockParams): void { + const { schema } = editor.state; + const { paragraph } = schema.nodes; + const docSize = editor.state.doc.content.size; + + if (from < 0 || to > docSize || from > to) { + console.error("Invalid range for replacement: ", from, to, "in a document of size", docSize); + return; + } + + // Split the textContent by new lines to handle each line as a separate paragraph for Windows (\r\n) and Unix (\n) + const lines = textContent.split(/\r?\n/); + const tr = editor.state.tr; + let insertPos = from; + + // Remove the code block first + tr.delete(from, to); + + // For each line, create a paragraph node and insert it + lines.forEach((line) => { + // if the line is empty, create a paragraph node with no content + const paragraphNode = line.length === 0 ? paragraph.create({}) : paragraph.create({}, schema.text(line)); + tr.insert(insertPos, paragraphNode); + insertPos += paragraphNode.nodeSize; + }); + + // Now persist the focus to the converted paragraph + const parentNodeOffset = findParentNode((node) => node.type === schema.nodes.codeBlock)(editor.state.selection)?.pos; + + if (parentNodeOffset === undefined) throw new Error("Invalid code block offset"); + + const lineNumber = getLineNumber(textContent, cursorPosInsideCodeblock, parentNodeOffset); + const cursorPosOutsideCodeblock = cursorPosInsideCodeblock + (lineNumber - 1); + + editor.view.dispatch(tr); + editor.chain().focus(cursorPosOutsideCodeblock).run(); +} + +/** + * Calculates the line number where the cursor is located inside the code block. + * Assumes the indexing of the content inside the code block is like ProseMirror's indexing. + * + * @param {string} textContent - The content of the code block. + * @param {number} cursorPosition - The absolute cursor position in the document. + * @param {number} codeBlockNodePos - The starting position of the code block node in the document. + * @returns {number} The 1-based line number where the cursor is located. + */ +function getLineNumber(textContent: string, cursorPosition: number, codeBlockNodePos: number): number { + // Split the text content into lines, handling both Unix and Windows newlines + const lines = textContent.split(/\r?\n/); + const cursorPosInsideCodeblockRelative = cursorPosition - codeBlockNodePos; + + let startPosition = 0; + let lineNumber = 0; + + for (let i = 0; i < lines.length; i++) { + // Calculate the end position of the current line + const endPosition = startPosition + lines[i].length + 1; // +1 for the newline character + + // Check if the cursor position is within the current line + if (cursorPosInsideCodeblockRelative >= startPosition && cursorPosInsideCodeblockRelative <= endPosition) { + lineNumber = i + 1; // Line numbers are 1-based + break; + } + + // Update the start position for the next line + startPosition = endPosition; + } + + return lineNumber; +} diff --git a/packages/editor/core/src/ui/extensions/custom-list-keymap/list-keymap.ts b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-keymap.ts index db1264f57..f2b6dd999 100644 --- a/packages/editor/core/src/ui/extensions/custom-list-keymap/list-keymap.ts +++ b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-keymap.ts @@ -69,7 +69,7 @@ export const ListKeymap = ({ tabIndex }: { tabIndex?: number }) => return handled; } catch (e) { - console.log("error in handling Backspac:", e); + console.log("Error in handling Delete:", e); return false; } }, @@ -104,7 +104,7 @@ export const ListKeymap = ({ tabIndex }: { tabIndex?: number }) => return handled; } catch (e) { - console.log("error in handling Backspac:", e); + console.log("Error in handling Backspace:", e); return false; } },