手把手写一个基于CodeMirror6的语言包

笔者在 2020 年的时候,写过一篇关于 CodeMirror5 的文章,当时 v6 还处于测试阶段,现在两年多过去了v6版本也已经很成熟。我们通过实现一个简单的全新的语言的方式,来熟悉 CodeMirror6。

需求说明

这是一款用于展示 SQL 执行日志的编辑器,根据日志信息的特点进行一下高亮:

  1. 每条日志信息都以「日期 时间[时区]」信息格式开头,对此进行特殊高亮,方便识别单条信息范围
  2. 每条日志信息都会有类型关键词(INFO/WARNING/ERROR)标识,对其进行特殊高亮,以方便用户快速识别信息的重要性
  3. 日志信息中可能展示 SQL 语句,需要对 SQL 语句的关键字进行高亮,以方便阅读
  4. 单条信息内容比较长的和块级注释内容,可以进行折叠,一次查阅更多信息

最后的执行效果如图:

按照 CodeMirror 语言包的习惯,将语言包命名为lang-sqllog,语言的函数名命名为 SQLLog,以下简称为 SQLLog。

SQLLog 实现

在 CodeMirror6 中实现一个语言包,通常要进行一下三步:

  1. 创建一个语法文件,用于解析语法
  2. 配置语法元数据属性,比如高亮、缩进、折叠信息
  3. 配置语言的特定扩展和命令,如关键词提示、快捷键等

SQLLog 只做展示使用,所以本文不涉及第3点(配置语言的特定扩展和命令)实现的讲解。

CodeMirror6 提供了一个示例语言包 codemirror/lang-example ,可以直接基于此进行开发,也可以自行搭建环境。此项不是本文的重点,不做展开描述。

语法解析

每种语言都有自己的特定语法规则,或简单或复杂。各类 IDE 中的编辑器之所以好用是因为,它们都能给出合适的配色(高亮)、缩进、折叠关系,以及给出合适的提示和错误提醒等等。前面提到的无论哪一种能力都需要结合语言特定语法来做,才能真正做到好用,否则只会"添乱"。

CodeMirror6 提供了 Lezer 这款自带的语法解析器,下面笔者也会以此解析器进行讲解。

语法定义

Lezer 跟 Antlr4 类似可以通过一个语法文件对语言进行描述定义。以下是笔者对 SQLLog 编写的一段语法文件:

文件名:sqlLog.grammar

tsx 复制代码
@top Program {
  Message { (DateTime ( Info | Warning | Error ) element+) } *
}

@skip { whitespace | LineComment | BlockComment }


element {
  String |
  Number |
  ErrorMsg |
  Identifier
}

@tokens {
  whitespace { $[ \t\n\r]+ }
  LineComment { "-""-" ![\n]* }
  DateTime { $[0-9]+ "-" $[0-9]+ "-" $[0-9]+ " " $[0-9]+ ":" $[0-9]+ ":" $[0-9]+ "[GMT+" $[0-9]+ ":" $[0-9]+ "]" }
  Info { "INFO" }
  Warning { "WARNING" }
  Error { "ERROR" }
  ErrorMsg { "ERROR_MSG" }
  Number { @digit + }
  String { '"' (!["\\] | "\\" _)* '"' }
  @precedence { DateTime, Number, ErrorMsg, Error }
}

@external tokens otherTokens from "./tokens" {
  Identifier
  BlockComment
  Bool
  Type
  Keyword
  Null
  Builtin
}

接下来笔者对以上文件内容一一做详细解释。

@top

@top 定义了语法入口点,整个代码块的含义如下:

示例中将入口节点命名为 Program (可修改),此语言的语法是有零或多个 Message (日志信息项,请记住这个名称,后面在"定义SQLLog语言"中会使用到)组成。单条 MessageDateTime 起始,然后是 InfoWarningError 中的一个,最后是多个 element 组成。

几个特殊符号的含义与正则表达式中的含义相似,如下:

  • *:重复 0 到无数次
  • +:重复 1 到无数次
  • ?:可选元素
  • |:选择其中之一

除了 element 其他规则名都是首字母大写的,凡大写字母起始的规则命名都会出现在解析器生成的语法书中(成为其中的一个节点),即 element 将不会在语法树中出现。这个特性可以简化语法树结构,移除不一样的节点嵌套,并且保证语法文件的结构清晰;简化了语法树的结构,也有利于提升解析语法树的性能。在较为复杂的语言解析过程中,遍历语法树是非常频繁的操作,比如关键词、语法提示。
element 表示由 StringNumberErrorMsgIdentifier其中之一构成,其定义的代码片段如下:

tsx 复制代码
element {
  String |
  Number |
  ErrorMsg |
  Identifier
}

ProgramMessage 的定义都已经在其后的大括号内完成,element 也已经解释,其他规则都在 @tokens@external tokens 中定义了,在相应章节进行具体说明。

@skip

@skip 定义了在解析过程中忽略、跳过的规则。代码含义是,对空白元素( whitespace)、行注释(LineComment)、块注释(BlockComment)进行忽略跳过处理。几乎所有的计算机语言对这些信息都是这样处理的。

@tokens

@tokens 中定义的都是直接可以用规则匹配出来的内容,是最小颗粒度的匹配。前面提到的"* + | ?"四个标识符依然适用,上述示例中使用到的规则还有:

  • $[ \t\n\r] 表示匹配空格、各种换行符的其中之一,含义与 | 相似
  • ![\n] 表示不匹配换行符,![] 表示不匹配[]中的内容
  • "-"或'"',标识匹配 - 或 ",单、双引号内的内容表示全匹配,如"INFO"就是标识匹配INFO字符串
  • @asciiLetter 匹配 $[a-zA-Z]
  • @asciiLowercase 匹配 $[a-z]
  • @asciiUppercase 匹配 $[A-Z]
  • @digit 匹配 $[0-9]
  • @whitespace 匹配任何 Unicode 标准定义的空白字符
  • @eof 匹配结尾

@precedence 重定义的是优先级,表示 DateTime 优先级高于 Number,ErrorMsg 优先级高于 Error,要做此优先级定义是因为这两对内容的定义存在冲突与歧义。如 DateTime 和 Number,DateTime 的定义中存在大量的 Number,如果没有这个定义解析器会不知道应该优先处理哪个。

@external tokens

Lexer 让笔者最为惊艳的就是 @external 能力,它可以直接从 ts(或js)文件中引入定义 tokens 的函数。这大大扩大了语法文件可以定义的范围,也让语法定义变得更为灵活。
@external tokens otherTokens from "./tokens" 的含义是从 ./tokens 文件中引入 otherTokens 对象,作为tokens。大括号内定义的是 token 规则名,这些规则名需要同 otherTokens 对象内的命名保持一致,详情请见"扩展文件"章节。

以上是本文中使用到的一些定义规则,如需了解更多,请参看帮助文档

语法编译

grammar 文件是不能直接被 JavaScript 引用的,需要经过编译后才能使用。Lezer 提供了变工具 @lezer/generator ,通过命令的方式执行:

powershell 复制代码
## 可以使用项目跟目录的相对路径
lezer-generator [grammar 路径/文件名] -o [输出 路径/文件名.js]

package.json 内的配置如下:

json 复制代码
{
  ......
  "scripts": {
    ......
    "lezer": "lezer-generator src/lang-sqllog/sqllog.grammar -o src/lang-sqllog/sqllog.grammar.js",
  },
  ......
  "devDependencies": {
    ......
    "@lezer/generator": "^1.2.3"
  }
}

如果使用的是 codemirror/lang-example 执行 npm run prepare 指令已经包含了 grammar 文件的变异,不需要额外处理。

编译后将生成 sqlLog.grammar.js 和 sqlLog.grammar.terms.js 两个文件,这两个文件都不需要做修改,直接使用即可。
文件名:sqlLog.grammar.js

javascript 复制代码
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {otherTokens} from "./tokens"
export const parser = LRParser.deserialize({
  version: 14,
  states: "!^Q]QROOObQRO'#CfOOQQ'#Cn'#CnQ]QROOOmQRO,59QOOQQ-E6l-E6lOOQQ'#Cs'#CsOOQQ'#Co'#CoO{QRO1G.lOOQQ-E6m-E6m",
  stateData: "!g~OfOSWOSQOS~OZPO~O[SO]SO^SO~OPUO_UO`UOaUO~OPUO_UO`UOaUOZYidYi~OZ`a^a~",
  goto: "}hPPPPPPPPPPiPPPPPPPmsPPPyTQORQRORTRQWSRXWTVSW",
  nodeNames: "⚠ Identifier BlockComment Bool Type Keyword Null Builtin LineComment Program Message DateTime Info Warning Error String Number ErrorMsg",
  maxTerm: 23,
  skippedNodes: [0,2,3,4,5,6,7,8],
  repeatNodeCount: 2,
  tokenData: "*R~RYXYqYZq]^qpqqrs!S}!O#p!Q![$_!g!h'f!k!l(o!y!z)W~vSf~XYqYZq]^qpqq~!VVOr!Srs!ls#O!S#O#P!q#P;'S!S;'S;=`#j<%lO!S~!qO_~~!tRO;'S!S;'S;=`!};=`O!S~#QWOr!Srs!ls#O!S#O#P!q#P;'S!S;'S;=`#j;=`<%l!S<%lO!S~#mP;=`<%l!S~#sP}!O#v~#{SW~OY#vZ;'S#v;'S;=`$X<%lO#v~$[P;=`<%l#v~$dQ`~}!O$j!Q![$_~$mP!Q![$p~$sQ}!O$y!Q![$p~$|P!Q![%P~%SQpq%Y!Q![%P~%]P!Q![%`~%cQ!Q![%`![!]%i~%lP!Q![%o~%rQ!Q![%o![!]%x~%{P!Q![&O~&RQ!Q![&O!}#O&X~&[P!i!j&_~&bP!o!p&e~&hP!v!w&k~&nP{|&q~&tP!Q![&w~&zQ!Q![&w![!]'Q~'TP!Q!['W~'ZQ!Q!['W#P#Q'a~'fOZ~~'iP!t!u'l~'oP!t!u'r~'uP!q!r'x~'{P!t!u(O~(TP^~#R#S(W~(ZP!o!p(^~(aP!u!v(d~(gP!i!j(j~(oOa~~(rP!p!q(u~(xP!h!i({~)OP!q!r)R~)WO[~~)ZP!c!d)^~)aP!t!u)d~)gP!p!q)j~)mP!k!l)p~)sP!p!q)v~)yP!i!j)|~*RO]~",
  tokenizers: [0, otherTokens],
  topRules: {"Program":[0,9]},
  tokenPrec: 63
})

此文件旨在生成解析器 parser,后面就是使用此解析器对预发进行解析生成语法树的。
文件名:sqlLog.grammar.terms.js

javascript 复制代码
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
  Identifier = 1,
  BlockComment = 2,
  Bool = 3,
  Type = 4,
  Keyword = 5,
  Null = 6,
  Builtin = 7,
  LineComment = 8,
  Program = 9,
  DateTime = 11,
  Info = 12,
  Warning = 13,
  Error = 14,
  String = 15,
  Number = 16,
  ErrorMsg = 17

此文件生成的是节点标识和编号的映射关系,在解析器生成的语法树中的每个节点标识都会以此处的编号出现。

扩展文件

本章节将详细介绍 ./tokens 文件中的内容,具体内容如下:

typescript 复制代码
import {ExternalTokenizer, InputStream} from '@lezer/lr';
import { BlockComment, Identifier, Bool, Null, Keyword, Type, Builtin } from './sqlLog.grammar.terms';

// 定义了各符号、字母、数字对应的 Ascii 码
const enum Ch {
  Newline = 10, // \n
  Space = 32, // 空格
  DoubleQuote = 34, // "
  Hash = 35, // #
  Dollar = 36, // $
  SingleQuote = 39, // '
  ParenL = 40, /* ( */ ParenR = 41,  /* ) */
  Star = 42, // *
  Plus = 43, // +
  Comma = 44, // ,
  Dash = 45, // -
  Dot = 46, // .
  Slash = 47, // /
  Colon = 58, // :
  Semi = 59, // ;
  Question = 63, // ?
  At = 64, // @
  BracketL = 91, /* [ */ BracketR = 93, /* ] */
  Backslash = 92, // \
  Underscore = 95, // _
  Backtick = 96, // `
  BraceL = 123, /* { */ BraceR = 125,  /* } */

  A = 65, a = 97,
  B = 66, b = 98,
  E = 69, e = 101,
  F = 70, f = 102,
  N = 78, n = 110,
  X = 88, x = 120,
  Z = 90, z = 122,

  _0 = 48, _1 = 49, _9 = 57,
}

function isAlpha(ch: number) {
  return ch >= Ch.A && ch <= Ch.Z || ch >= Ch.a && ch <= Ch.z || ch >= Ch._0 && ch <= Ch._9;
}


function readWord(input: InputStream, result: string): string {
  for (;;) {
    // 所匹配的字符只能是数字、字母和下划线
    if (input.next != Ch.Underscore && !isAlpha(input.next)) break
    if (result != null) result += String.fromCharCode(input.next)
    input.advance()
  }
  return result
}

function keywords(keywords: string, types: string, builtin?: string) {
  let result: {[name: string]: number} = Object.create(null)
  // 匹配布尔类型
  result["true"] = result["false"] = Bool
  // 匹配 Null
  result["null"] = result["unknown"] = Null
  // 匹配关键字
  for (let kw of keywords.split(" ")) if (kw) result[kw] = Keyword
  // 匹配数据类型
  for (let tp of types.split(" ")) if (tp) result[tp] = Type
  return result
}

// 定义SQL的数据类型
export const SQLTypes = "array binary bit boolean char character clob date decimal double float int integer interval large national nchar nclob numeric object precision real smallint time timestamp varchar varying "
// 定义SQL的关键字
export const SQLKeywords = "absolute action add after all allocate alter and any are as asc assertion at authorization before begin between both breadth by call cascade cascaded case cast catalog check close collate collation column commit condition connect connection constraint constraints constructor continue corresponding count create cross cube current current_date current_default_transform_group current_transform_group_for_type current_path current_role current_time current_timestamp current_user cursor cycle data day deallocate declare default deferrable deferred delete depth deref desc describe descriptor deterministic diagnostics disconnect distinct do domain drop dynamic each else elseif end end-exec equals escape except exception exec execute exists exit external fetch first for foreign found from free full function general get global go goto grant group grouping handle having hold hour identity if immediate in indicator initially inner inout input insert intersect into is isolation join key language last lateral leading leave left level like limit local localtime localtimestamp locator loop map match method minute modifies module month names natural nesting new next no none not of old on only open option or order ordinality out outer output overlaps pad parameter partial path prepare preserve primary prior privileges procedure public read reads recursive redo ref references referencing relative release repeat resignal restrict result return returns revoke right role rollback rollup routine row rows savepoint schema scroll search second section select session session_user set sets signal similar size some space specific specifictype sql sqlexception sqlstate sqlwarning start state static system_user table temporary then timezone_hour timezone_minute to trailing transaction translation treat trigger under undo union unique unnest until update usage user using value values view when whenever where while with without work write year zone "

const words = keywords(SQLKeywords, SQLTypes);


export const otherTokens = new ExternalTokenizer((input, stack) => {
  // 只有未匹配 token 的内容才会进入此方法

  // next 是当前字符的 Ascii 码
  const { next } = input;
  input.advance();

  if (next == Ch.Slash && input.next == Ch.Star) {
    input.advance()
    // 进行多层匹配处理,如果块注释内存在块注释则认为还是在同一个块注释内
    for (let depth = 1;;) {
      let cur: number = input.next
      if (input.next < 0) break
      input.advance()
      if (cur == Ch.Star && (input as any).next == Ch.Slash) {
        depth--
        input.advance()
        if (!depth) break
      } else if (cur == Ch.Slash && input.next == Ch.Star) {
        depth++
        input.advance()
      }
    }
    // 匹配块注释
    input.acceptToken(BlockComment)
  } else if (isAlpha(next)) {
    let word = readWord(input, String.fromCharCode(next))
    // 凡是未匹配上 keywords 方法中定义的规则的都认为是标识符
    input.acceptToken(input.next == Ch.Dot ? Identifier : words[word.toLowerCase()] ?? Identifier)
  }
});

此文件的内容是笔者从 codemirror/lang-sql 改造而来,旨在定义块注释、字符串、SQL 关键字、SQL 类型等 tokens,即 @external tokens 中的内容。文件中是标准的 ts 语法,不做一一解释了,关键点加了注释说明。使用 input.acceptToken注入的 token 规则名,表示所匹配的内容属于该token。

定义 SQLLog 语言

前面所述的语法解析,最终是为了在定义语言的时候所使用,可以使用 Language 也可以使用 LRLanguage 用于绑定 Lezer LR 解析器。示例中使用了 LRLanguage 来实现,如下:

typescript 复制代码
import {continuedIndent, indentNodeProp, foldNodeProp, LRLanguage, LanguageSupport} from '@codemirror/language';
import {Extension} from '@codemirror/state';
import { styleTags, tags as t } from '@lezer/highlight';
import {parser} from './sqlLog.grammar.js';

const language = LRLanguage.define({
  name: "SQLLog",
  // 可以直接将 parser 给 parser,使用 parser.configure 可配置缩进、折叠、高亮属性
  parser: parser.configure({
    props: [
      // 配置缩进属性,SQLLog不需要进行编辑,也就没有自动缩进的需要
      // indentNodeProp.add({
      //   Message: continuedIndent()
      // }),
      // 配置折叠属性
      foldNodeProp.add({
        // 对"语法文件"中的 Message 规则(日志信息)进行折叠处理
        Message(tree) { return {from: tree.firstChild!.to, to: tree.to} },
        // 对"语法文件"中的 BlockComment 规则(块注释)进行折叠处理
        BlockComment(tree) { return {from: tree.from + 2, to: tree.to - 2} }
      }),
      // 建立 token 和 style 的映射关系
      styleTags({
        // 将 DateTime token 应用 heading 样式
        DateTime: t.heading,
        // typeName 和 strong 的两个特性会同时生效
        Info: [t.typeName, t.strong],
        Warning:[t.comment, t.strong],
        Error: [t.invalid, t.strong],
        Keyword: t.keyword,
        ErrorMsg: t.invalid,
        Type: t.typeName,
        Builtin: t.name,
        Bool: t.bool,
        Null: t.null,
        Number: t.number,
        String: t.string,
        LineComment: t.lineComment,
        BlockComment: t.blockComment
      })
    ]
  }), 
})

// 此方法可以直接应用于 EditorView 或 EditorState 中
export function SQLLog() {
  // 使用 LanguageSupport 可以将语言定义和扩展程序绑定在一起,本示例中没有扩展能力,所以没传第二个参数
  return new LanguageSupport(language)
}

至此 SQLLog 语言已经定义完成,最初的需求也都已经实现,接下来只要在编辑器应用此语言包即可。

应用示例

示例使用 ReactJS 的方式进行初始化,具体代码如下:

tsx 复制代码
import { render } from 'react-dom';
import React, { memo, ReactElement, useEffect, useRef } from 'react';
import { EditorView, basicSetup } from 'codemirror';
import { SQLLog } from './lang-sqllog/sqlLog';

export default memo(() => {
  const conRef = useRef<ReactElement>();

  useEffect(() => {
    const state = new EditorView({
      // 初始化文本内容
      doc: document,
      extensions: [
        // 所有的基础配置都是通过 basicSetup 实现的,如行号、默认皮肤、高亮样式等等
        basicSetup,
        // 应用 SQLLog 语言包
        SQLLog(),
      ],
      // 指定渲染容器
      parent: conRef.current,
    });
  }, []);

  return <div className="editor" ref={conRef} />;
});

render(<div className="box"><SQLLogEditor /></div>, document.getElementById('root'));


const document = `2023-06-07 15:40:02[GMT+08:00] INFO - Resource Control is active!
2023-06-07 15:40:02[GMT+08:00] INFO - try take resource Lock
2023-06-07 15:40:02[GMT+08:00] WARNING - try take resource Lock
2023-06-07 15:40:02[GMT+08:00] ERROR - try take resource Lock
2023-06-07 15:40:02[GMT+08:00] INFO - take lock success! lock info:ResourceLock{resourceType=[DATABASE], resourceId=[000000], runningJobs=[701664-f_173-j_613-5,701664-f_173-j_612-5,71394...]}
2023-06-07 15:40:02[GMT+08:00] INFO - Starting job j_628 at Wed Jun 07 15:40:02 CST 2023
2023-06-07 15:40:12[GMT+08:00] INFO - Sql_job logs from XXXX:
------[2023-06-07 15:40:03]------

当前表位空,忽略此操作. cost:9 ms

------[2023-06-07 15:40:03]------
jobSuccess callback End.


2023-06-07 15:40:12[GMT+08:00] INFO - SQL[
/* 
  * 我是块级注释说明
  * 我是块级注释说明
*/

INSERT INTO courses 
SELECT * FROM courses WHERE name17 = 2023-06-06 and name17 = 2023-06-06 and name17=1], affect rows: 0
2023-06-07 15:40:13[GMT+08:00] INFO - Crawl lineage async.
2023-06-07 15:40:13[GMT+08:00] INFO - Finishing job j_628 at 00000000000000 with status SUCCEEDED
2023-06-07 11:40:11[GMT+08:00] INFO - Resource Control is active!
2023-06-07 11:40:11[GMT+08:00] INFO - try take resource Lock
2023-06-07 11:40:11[GMT+08:00] INFO - Parsing executing sql:
create table if not exists blood_b (
    id int,
    name varchar(256),
    age int,
    address text
);


insert into blood_b
select * from blood_a;

create table if not exists blood_f like \`blood_a\`;
2023-06-07 11:40:11[GMT+08:00] INFO - Verifying authorization : user[0000000000] database[0000000000]
2023-06-07 11:40:21[GMT+08:00] INFO - Sql_job logs from XXXX:
------[2023-06-07 11:40:11]------
创建任务:分组id:0;分组排序:0

------[2023-06-07 11:40:15]------
jobSuccess callback Start.

------[2023-06-07 11:40:15]------
start database level metadata sync task [Master]...

------[2023-06-07 11:40:16]------

database table level sync. cost:593 ms

------[2023-06-07 11:40:16]------
jobSuccess callback End.


2023-06-07 11:40:21[GMT+08:00] INFO - SQL[


insert into blood_b
select * from blood_a], affect rows: 0
2023-06-07 11:40:22[GMT+08:00] INFO - Crawl lineage async.
2023-06-07 14:55:00[GMT+08:00] INFO - Parsing executing sql:
/* 请使用当前节点所选择的数据库语法编写SQL */
SELECT * FROM \`ex_customer\` 
 LIMIT 20;
2023-06-07 14:55:00[GMT+08:00] INFO - Verifying authorization : user[0000000000] database[0000000000]
2023-06-07 14:55:00[GMT+08:00] INFO - Starting Sql query execution. Query key:SqlAssignmentJob-11111111111111
2023-06-07 14:55:01[GMT+08:00] INFO - Query success! result List:
	customer_id	sex	customer_name	age	province	city	register_time
	1	男	邵**	95	陕西省	宜昌市	1989-04-16 01:00:00
	2	男	丁**	95	重庆市	铜陵市	2011-06-08 18:57:08
	3	男	卢**	92	山东省	晋中市	2010-06-13 18:52:13
	4	女	许**	23	吉林省	怒江傈僳族自治州	2015-11-24 10:17:04
	5	男	戚**	87	广西壮族自治区	大庆市	2011-03-02 03:53:07
	6	男	尹**	57	福建省	岳阳市	2010-04-05 15:16:29
	7	女	嵇**	29	江西省	南平市	2018-11-28 07:20:59
	8	女	丘**	71	浙江省	云浮市	2013-09-16 21:47:46
	9	女	祝**	33	安徽省	省直辖县级行政区划	2013-03-05 08:10:24
	10	男	史**	73	广西壮族自治区	张掖市	2017-10-13 23:25:57
	11	男	邵**	95	陕西省	宜昌市	2010-09-28 17:29:25
	12	男	丁**	95	重庆市	铜陵市	2011-06-08 18:57:08
	13	男	卢**	92	山东省	晋中市	2010-06-13 18:52:13
	14	女	许**	23	吉林省	怒江傈僳族自治州	2015-11-24 10:17:04
	15	男	戚**	87	广西壮族自治区	大庆市	2011-03-02 03:53:07
	16	男	尹**	57	福建省	岳阳市	2010-04-05 15:16:29
	17	女	嵇**	29	江西省	南平市	2018-11-28 07:20:59
	18	女	丘**	71	浙江省	云浮市	2013-09-16 21:47:46
	19	女	祝**	33	安徽省	省直辖县级行政区划	2013-03-05 08:10:24
	20	男	史**	73	广西壮族自治区	张掖市	2017-10-13 23:25:57
	
2023-06-07 14:55:01[GMT+08:00] INFO - Assigning Node Variables...
2023-06-07 14:55:01[GMT+08:00] INFO - Variable0[q1]: "1"
2023-06-07 14:55:01[GMT+08:00] INFO - Variable0[q1] assigned
2023-06-07 14:55:01[GMT+08:00] INFO - Finishing job j_612 at 0000000000 with status SUCCEEDED
2023-06-07 15:19:41[GMT+08:00] INFO - Resource Control is active!
2023-06-07 15:19:46[GMT+08:00] INFO - Starting to run job.
2023-06-07 15:20:02[GMT+08:00] INFO - Print task detail log.
2023-06-07 15:19:57[GMT+08:00] INFO - Preparing spark sql job...
2023-06-07 15:19:57[GMT+08:00] INFO - Execute spark task with resource: driver cores 2, driver memory 8g, executor number 2, executor cores 2, executor memory 20g
2023-06-07 15:19:57[GMT+08:00] INFO - SQL to execute: 
/* 请使用Spark SQL的语法编写SQL,表的引用方式为 alias.table_name */ 
create table xxxx_mysql.user_test as select uuid() as id, 'zcf' as name;
insert into xxxx_mysql.user_test select uuid(), 'zcf1';
insert into xxxx_mysql.user_test select uuid(), 'zcf2';

2023-06-07 15:19:57[GMT+08:00] INFO - Resolve variables finished: 
/* 请使用Spark SQL的语法编写SQL,表的引用方式为 alias.table_name */ 
create table xxxx_mysql.user_test as select uuid() as id, 'zcf' as name;
insert into xxxx_mysql.user_test select uuid(), 'zcf1';
insert into xxxx_mysql.user_test select uuid(), 'zcf2';

2023-06-07 15:19:57[GMT+08:00] INFO - Parse table names successfully: ["xxxx_mysql.user_test"]

2023-06-07 15:19:57[GMT+08:00] INFO - Parsing used table.

2023-06-07 15:19:58[GMT+08:00] INFO - Prepare task finished.
2023-06-07 15:19:58[GMT+08:00] INFO - Execute spark sql: create database ss_1_2016411_0e29

2023-06-07 15:20:01[GMT+08:00] INFO - Execute spark sql: use ss_1_2016411_0e29

2023-06-07 15:20:01[GMT+08:00] INFO - creating temp views...

2023-06-07 15:20:12[GMT+08:00] INFO - Print task detail log.

2023-06-07 15:20:09[GMT+08:00] INFO - Execute spark sql: INSERT INTO TABLE \`xxxx_mysql_user_test_10753_0\`
SELECT uuid(), 'zcf1'
2023-06-07 15:20:22[GMT+08:00] INFO - Print task detail log.

2023-06-07 15:20:16[GMT+08:00] INFO - Execute spark sql: INSERT INTO TABLE \`xxxx_mysql_user_test_10753_0\`
SELECT uuid(), 'zcf2'

2023-06-07 15:20:17[GMT+08:00] INFO - Execute spark sql: drop database ss_1_2016411_0e29 CASCADE
2023-06-07 15:20:32[GMT+08:00] INFO - Spark sql job completed.
2023-06-07 15:20:32[GMT+08:00] INFO - Crawl lineage async.
2023-06-07 15:19:26[GMT+08:00] INFO - Print task detail log.
2023-06-07 15:19:24[GMT+08:00] INFO - Preparing spark sql job...
2023-06-07 15:19:24[GMT+08:00] INFO - Execute spark task with resource: driver cores 2, driver memory 8g, executor number 2, executor cores 2, executor memory 20g
2023-06-07 15:19:24[GMT+08:00] INFO - SQL to execute: 
/* 请使用Spark SQL的语法编写SQL,表的引用方式为 alias.table_name */ 
create table xxxx_mysql.user_test as select uuid() as id, 'zcf' as name;
insert into xxxx_mysql.user_test select uuid(), 'zcf1';
insert into xxxx_mysql.user_test

2023-06-07 15:19:24[GMT+08:00] INFO - Resolve variables finished: 
/* 请使用Spark SQL的语法编写SQL,表的引用方式为 alias.table_name */ 
create table xxxx_mysql.user_test as select uuid() as id, 'zcf' as name;
insert into xxxxo_mysql.user_test select uuid(), 'zcf1';
insert into xxxx_mysql.user_test

2023-06-07 15:19:24[GMT+08:00] ERROR - Error occur when execute task.
ERROR_MSG: com.xxxxxx.xxxxxx.sql.parser.ParserException: syntax error, error in :'ysql.user_test
', expect SELECT, actual null, pos 215, line 5, column 2, token EOF
2023-06-07 15:19:24[GMT+08:00] INFO - ERROR_MSG: Prepare task failed.
2023-06-07 15:19:26[GMT+08:00] ERROR - Job run failed!
SparkSqlJobProcessException: Execute job failed.
2023-06-07 15:19:26[GMT+08:00] INFO - Finishing job j_10753 at 00000000000000 with status FAILED`;

总结

本文以 SQLLog 为例讲解了基于 CodeMirror6 自定义语言包的过程,主要步骤总结如下:

  1. 使用 CodeMirror6 自带的 Lexer 描述语言进行语法定义;使用 @top 定义语法入口,@skip 定义忽略跳过内容,@tokens 定义tokens。如遇特别复杂,或者需要支持参数传入的内容可使用 @external tokens 方式使用 JS 实现
  2. 对 grammar 文件通过 @lezer/generator 进行编译,转换成 JS 文件
  3. 使用 LRLanguage 对象讲语法解析器和语法定义进行绑定,并配置缩进、折叠、高亮等内容;如有预发提示灯内容也在此环节实现,文本未涉及
  4. 以上三步实现了语言包中的所有内容,最后再 EditorView 中进行使用

附录

CodeMirror6 官网:codemirror.net/

GitHub 地址:github.com/codemirror

相关推荐
Jiaberrr7 分钟前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho1 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
太阳花ˉ2 小时前
html+css+js实现step进度条效果
javascript·css·html
小白学习日记2 小时前
【复习】HTML常用标签<table>
前端·html
john_hjy3 小时前
11. 异步编程
运维·服务器·javascript
风清扬_jd3 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
丁总学Java3 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele3 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范