From 26659737663c8b7a741a28ad632b68d1bd41e0c1 Mon Sep 17 00:00:00 2001 From: isletspace Date: Wed, 9 Sep 2026 14:58:15 +0800 Subject: [PATCH] feat(editor): add language selector to code block node view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code blocks created via slash command, paste, or markdown import get a null language and there is no UI to set or change it afterwards — the only path is retyping the ```lang fence from scratch. This adds a small dropdown at the top-left of code blocks (editable mode only) listing every grammar registered in lowlight plus a Plain text (null) option, writing through to the node's language attribute so highlighting updates immediately. --- .../extensions/code/code-block-node-view.tsx | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/packages/editor/src/core/extensions/code/code-block-node-view.tsx b/packages/editor/src/core/extensions/code/code-block-node-view.tsx index 6df877d44b6..9cb6e44663b 100644 --- a/packages/editor/src/core/extensions/code/code-block-node-view.tsx +++ b/packages/editor/src/core/extensions/code/code-block-node-view.tsx @@ -4,12 +4,12 @@ * See the LICENSE file for details. */ -import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import type { NodeViewProps } from "@tiptap/react"; import { NodeViewWrapper, NodeViewContent } from "@tiptap/react"; import ts from "highlight.js/lib/languages/typescript"; import { common, createLowlight } from "lowlight"; import { CheckIcon } from "lucide-react"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { CopyIcon } from "@plane/propel/icons"; // ui import { Tooltip } from "@plane/propel/tooltip"; @@ -23,14 +23,27 @@ import { ECodeBlockAttributeNames } from "./types"; const lowlight = createLowlight(common); lowlight.register("ts", ts); -type Props = { - node: ProseMirrorNode; -}; - -export function CodeBlockComponent({ node }: Props) { +export function CodeBlockComponent(props: NodeViewProps) { + const { node, editor, updateAttributes } = props; const [copied, setCopied] = useState(false); // derived values const attrs = node.attrs as TCodeBlockAttributes; + const currentLanguage = attrs[ECodeBlockAttributeNames.LANGUAGE] ?? ""; + + // languages supported by the registered lowlight grammars + const languageOptions = useMemo(() => { + const options = lowlight.listLanguages().toSorted((a, b) => a.localeCompare(b)); + // keep an explicitly set but unregistered language selectable instead of blanking the picker + if (currentLanguage && !options.includes(currentLanguage)) { + options.push(currentLanguage); + } + return options; + }, [currentLanguage]); + + const handleLanguageChange = (e: React.ChangeEvent) => { + const language = e.target.value; + updateAttributes({ [ECodeBlockAttributeNames.LANGUAGE]: language === "" ? null : language }); + }; const copyToClipboard = async (e: React.MouseEvent) => { try { @@ -46,6 +59,28 @@ export function CodeBlockComponent({ node }: Props) { return ( + {editor.isEditable && ( +
e.stopPropagation()} + > + +
+ )}