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

如何基于Next.js+pnpm实现仅高亮大括号的Monaco Editor自定义语言

解决方案

1. 修正Monarch Tokenizer,实现括号内文本高亮

现有代码的tokenizer仅标记了大括号本身,未处理括号内的文本。需要通过状态切换实现括号内内容的绿色标记:

export const languageDef = {
  defaultToken: 'plain',
  brackets: [['{', '}', 'delimiter.curly']],
  tokenizer: {
    root: [
      // 匹配左括号,切换到inside状态并标记括号
      [/{/, { token: 'delimiter.curly.open', next: '@inside' }],
      // 匹配右括号直接标记
      [/}/, 'delimiter.curly.close'],
      // 非括号文本使用默认样式
      [/[^{}]+/, 'plain'],
    ],
    inside: [
      // 匹配右括号,切回root状态并标记括号
      [/}/, { token: 'delimiter.curly.close', next: '@pop' }],
      // 括号内的文本标记为green-text
      [/[^{}]+/, 'green-text'],
    ],
  },
}

2. 调整主题样式,对应token颜色

修改主题配置,为不同token分配指定颜色:

  • 正常大括号设为黑色(可根据需求调整)
  • 括号内文本设为绿色
  • 预留错误token的红色样式
const configuration = {
  base: 'vs',
  inherit: true,
  rules: [
    { token: 'delimiter.curly.open', foreground: '000000' },
    { token: 'delimiter.curly.close', foreground: '000000' },
    { token: 'green-text', foreground: '008000' },
    { token: 'error.curly', foreground: 'FF0000', fontStyle: 'bold' },
  ],
}

3. 添加语法诊断,标记未闭合/多余括号

通过注册诊断提供者,扫描文本中的括号匹配情况,生成错误提示:

在useEffect中补充以下代码:

// 配置语言的括号自动闭合规则
monaco.languages.setLanguageConfiguration('tracta', {
  brackets: [['{', '}']],
  autoClosingPairs: [{ open: '{', close: '}' }],
})

// 注册诊断提供者,检查括号匹配
monaco.languages.registerDiagnosticsProvider('tracta', {
  provideDiagnostics: (model) => {
    const text = model.getValue()
    const diagnostics = []
    const stack = []

    for (let i = 0; i < text.length; i++) {
      const char = text[i]
      if (char === '{') {
        stack.push(i)
      } else if (char === '}') {
        if (stack.length === 0) {
          // 多余的右括号
          const pos = model.getPositionAt(i)
          diagnostics.push({
            severity: monaco.MarkerSeverity.Error,
            startLineNumber: pos.lineNumber,
            startColumn: pos.column,
            endLineNumber: pos.lineNumber,
            endColumn: pos.column + 1,
            message: '多余的闭合大括号',
            source: 'tracta',
          })
        } else {
          stack.pop()
        }
      }
    }

    // 未闭合的左括号
    stack.forEach((pos) => {
      const position = model.getPositionAt(pos)
      diagnostics.push({
        severity: monaco.MarkerSeverity.Error,
        startLineNumber: position.lineNumber,
        startColumn: position.column,
        endLineNumber: position.lineNumber,
        endColumn: position.column + 1,
        message: '未闭合的大括号',
        source: 'tracta',
      })
    })

    return diagnostics
  },
})

完整组件代码

整合所有修改后的完整代码:

import React, { useEffect } from 'react'
import Editor, { useMonaco } from '@monaco-editor/react'

export const languageDef = {
  defaultToken: 'plain',
  brackets: [['{', '}', 'delimiter.curly']],
  tokenizer: {
    root: [
      [/{/, { token: 'delimiter.curly.open', next: '@inside' }],
      [/}/, 'delimiter.curly.close'],
      [/[^{}]+/, 'plain'],
    ],
    inside: [
      [/}/, { token: 'delimiter.curly.close', next: '@pop' }],
      [/[^{}]+/, 'green-text'],
    ],
  },
}

const configuration = {
  base: 'vs',
  inherit: true,
  rules: [
    { token: 'delimiter.curly.open', foreground: '000000' },
    { token: 'delimiter.curly.close', foreground: '000000' },
    { token: 'green-text', foreground: '008000' },
    { token: 'error.curly', foreground: 'FF0000', fontStyle: 'bold' },
  ],
}

export default function TractaEditor({
  value,
  onChange,
}: {
  value: string
  onChange: (value: string | undefined) => void
}) {
  const monaco = useMonaco()

  useEffect(() => {
    if (!monaco) return

    monaco.languages.register({ id: 'tracta' })
    monaco.languages.setMonarchTokensProvider('tracta', languageDef)

    monaco.languages.setLanguageConfiguration('tracta', {
      brackets: [['{', '}']],
      autoClosingPairs: [{ open: '{', close: '}' }],
    })

    monaco.languages.registerDiagnosticsProvider('tracta', {
      provideDiagnostics: (model) => {
        const text = model.getValue()
        const diagnostics = []
        const stack = []

        for (let i = 0; i < text.length; i++) {
          const char = text[i]
          if (char === '{') {
            stack.push(i)
          } else if (char === '}') {
            if (stack.length === 0) {
              const pos = model.getPositionAt(i)
              diagnostics.push({
                severity: monaco.MarkerSeverity.Error,
                startLineNumber: pos.lineNumber,
                startColumn: pos.column,
                endLineNumber: pos.lineNumber,
                endColumn: pos.column + 1,
                message: '多余的闭合大括号',
                source: 'tracta',
              })
            } else {
              stack.pop()
            }
          }
        }

        stack.forEach((pos) => {
          const position = model.getPositionAt(pos)
          diagnostics.push({
            severity: monaco.MarkerSeverity.Error,
            startLineNumber: position.lineNumber,
            startColumn: position.column,
            endLineNumber: position.lineNumber,
            endColumn: position.column + 1,
            message: '未闭合的大括号',
            source: 'tracta',
          })
        })

        return diagnostics
      },
    })

    monaco.editor.defineTheme('tracta', configuration)
  }, [monaco])

  return (
    <Editor
      defaultLanguage="tracta"
      theme="tracta"
      value={value}
      onChange={onChange}
      line={20}
      options={{
        lineNumbers: 'off',
        minimap: { enabled: false },
        readOnly: false,
        scrollbar: { vertical: 'hidden', horizontal: 'hidden' },
        wordWrap: 'on',
        wrappingIndent: 'same',
        fontFamily: 'Geist',
        fontSize: 14,
        lineHeight: 20,
        lineDecorationsWidth: 0,
        glyphMargin: false,
        renderLineHighlight: 'none',
        overviewRulerBorder: false,
        folding: false,
        rulers: [],
      }}
      className="flex max-h-[120px] min-h-[80px] w-full rounded-base border-2 border-black bg-white p-2 text-sm font-base shadow-base ring-offset-white placeholder:text-black/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
    />
  )
}

关键说明

  • Tokenizer状态管理:通过@inside和@pop实现括号内外的状态切换,精准标记不同区域的文本样式。
  • 诊断逻辑:手动扫描文本中的括号匹配关系,生成错误标记,实现未闭合/多余括号的红色提示。
  • 主题映射:将自定义token与颜色绑定,确保视觉效果符合需求。

内容的提问来源于stack exchange,提问作者charlietlamb

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 15:28:19