You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 00:07:47