如何在React的Monaco Editor中渲染图标实现行级注释交互?
如何在React的Monaco Editor中为代码行添加带注释的图标?
需求:开发代码片段展示平台,允许创作者为代码片段添加逐行注释,点击代码行右侧图标触发tooltip/弹窗展示注释内容。
初始代码:
"use client"; // import { languages } from "@/config/languages"; import { Editor, loader } from "@monaco-editor/react"; import { githubDarkTheme } from "@/config/themes/github-dark"; import { useEffect } from "react"; export default function CodeEditor({ language, theme, fontSize, }: { language: string; theme: string; fontSize: number; }) { useEffect(() => { loader.init().then((monaco) => { monaco.editor.defineTheme("github-dark", githubDarkTheme as any); }); }, []); return ( <> <Editor height="30vh" theme={theme} defaultLanguage={language} defaultValue="// start writing code here ..." options={{ fontFamily: "Fira Code, monospace", fontLigatures: true, fontSize: fontSize, padding: { top: 15 }, }} onMount={(editor, monaco) => { console.log("onMount", editor, monaco); }} /> </> ); }
解决方案:使用Monaco Editor的装饰器(Decorations)实现
Monaco Editor提供了装饰器API,可以在代码行的指定位置插入自定义DOM元素,这是实现需求的官方途径。具体步骤如下:
1. 准备装饰器样式
先在全局CSS中添加图标和tooltip的样式:
/* 注释图标样式 */ .code-comment-icon { position: absolute; right: 10px; cursor: pointer; color: #6e7681; transition: color 0.2s; } .code-comment-icon:hover { color: #3fb950; } /* Tooltip样式 */ .code-comment-tooltip { position: absolute; background: #1f2937; color: #f9fafb; padding: 8px 12px; border-radius: 4px; font-size: 14px; z-index: 1000; white-space: pre-wrap; max-width: 300px; }
2. 在Editor的onMount回调中实现装饰逻辑
修改onMount函数,创建装饰器并绑定点击事件:
onMount={(editor, monaco) => { // 模拟带注释的代码行数据,实际可从后端或props获取 const lineComments = [ { lineNumber: 1, comment: "这是代码的起始注释,提示用户开始编写代码" }, { lineNumber: 4, comment: "这里是打印hello world的示例代码" }, ]; // 定义装饰器选项 const decorations = lineComments.map(item => ({ range: new monaco.Range(item.lineNumber, 1, item.lineNumber, 1), options: { // 在行后添加自定义元素容器 afterContentClassName: "code-comment-icon-container", // 自定义渲染内容 renderOptions: { afterContent: { element: () => { const icon = document.createElement("span"); icon.className = "code-comment-icon"; icon.textContent = "💬"; // 可替换为SVG图标或自定义字符 icon.title = "点击查看注释"; // 绑定点击事件,显示tooltip icon.addEventListener("click", (e) => { e.stopPropagation(); // 移除已存在的tooltip const existingTooltip = document.querySelector(".code-comment-tooltip"); if (existingTooltip) existingTooltip.remove(); // 创建tooltip元素 const tooltip = document.createElement("div"); tooltip.className = "code-comment-tooltip"; tooltip.textContent = item.comment; // 设置tooltip位置 const rect = icon.getBoundingClientRect(); tooltip.style.top = `${rect.top - tooltip.offsetHeight - 5}px`; tooltip.style.left = `${rect.left}px`; document.body.appendChild(tooltip); // 点击其他区域关闭tooltip const closeTooltip = () => { tooltip.remove(); document.removeEventListener("click", closeTooltip); }; document.addEventListener("click", closeTooltip); }); return icon; } } } } })); // 将装饰器应用到编辑器 editor.deltaDecorations([], decorations); // 监听编辑器滚动,同步关闭tooltip(可选) editor.onDidScrollChange(() => { const tooltip = document.querySelector(".code-comment-tooltip"); if (tooltip) tooltip.remove(); }); }}
3. 关键API说明
editor.deltaDecorations: 用于添加、更新或移除编辑器装饰器,第一个参数是要移除的装饰器ID数组,第二个是新的装饰器数组。monaco.Range: 指定装饰器应用的范围,这里设置为单行的起始位置。renderOptions.afterContent.element: 自定义渲染的DOM元素,返回一个DOM节点即可,可绑定任意事件。
完整修改后的组件代码
"use client"; // import { languages } from "@/config/languages"; import { Editor, loader } from "@monaco-editor/react"; import { githubDarkTheme } from "@/config/themes/github-dark"; import { useEffect } from "react"; // 引入全局样式文件 import "./CodeEditor.css"; export default function CodeEditor({ language, theme, fontSize, }: { language: string; theme: string; fontSize: number; }) { useEffect(() => { loader.init().then((monaco) => { monaco.editor.defineTheme("github-dark", githubDarkTheme as any); }); }, []); return ( <> <Editor height="30vh" theme={theme} defaultLanguage={language} defaultValue="// start writing code here ...\n\nfunction example() {\n console.log('hello world');\n}" options={{ fontFamily: "Fira Code, monospace", fontLigatures: true, fontSize: fontSize, padding: { top: 15, right: 40 }, // 右侧留空间放图标,避免遮挡代码 }} onMount={(editor, monaco) => { // 模拟带注释的代码行数据,实际可从props传入 const lineComments = [ { lineNumber: 1, comment: "这是代码的起始注释,提示用户开始编写代码" }, { lineNumber: 4, comment: "这里是打印hello world的示例代码" }, ]; const decorations = lineComments.map(item => ({ range: new monaco.Range(item.lineNumber, 1, item.lineNumber, 1), options: { afterContentClassName: "code-comment-icon-container", renderOptions: { afterContent: { element: () => { const icon = document.createElement("span"); icon.className = "code-comment-icon"; icon.textContent = "💬"; icon.title = "点击查看注释"; icon.addEventListener("click", (e) => { e.stopPropagation(); const existingTooltip = document.querySelector(".code-comment-tooltip"); if (existingTooltip) existingTooltip.remove(); const tooltip = document.createElement("div"); tooltip.className = "code-comment-tooltip"; tooltip.textContent = item.comment; const rect = icon.getBoundingClientRect(); tooltip.style.top = `${rect.top - tooltip.offsetHeight - 5}px`; tooltip.style.left = `${rect.left}px`; document.body.appendChild(tooltip); const closeTooltip = () => { tooltip.remove(); document.removeEventListener("click", closeTooltip); }; document.addEventListener("click", closeTooltip); }); return icon; } } } } })); editor.deltaDecorations([], decorations); // 滚动时关闭tooltip editor.onDidScrollChange(() => { const tooltip = document.querySelector(".code-comment-tooltip"); if (tooltip) tooltip.remove(); }); }} /> </> ); }
注意事项
- 编辑器的
options.padding.right要设置足够的空间,避免图标遮挡代码。 - 如果注释数据是动态变化的,需要监听数据变化,重新调用
editor.deltaDecorations更新装饰器。 - 可以用SVG图标替代表情,提升视觉效果,只需要修改
icon.textContent为<img>标签或内联SVG代码即可。
内容的提问来源于stack exchange,提问作者devvMuhammad
相关产品推荐
相关产品推荐

