數棧產品中的程式碼編譯器

2023-11-15 12:02:49

我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式資料中臺產品。我們始終保持工匠精神,探索前端道路,為社群積累並傳播經驗價值。

本文作者:奇銘

前言

目前數棧的多個產品中都支援線上編輯 SQL 來生成對應的任務。比如離線開發產品和實時開發產品。在使用 MonacoEditor 為編輯器的基礎上,我們還支援瞭如下幾個重要功能:

  • 多種 SQL 的語法高亮
  • 多種 SQL 的報錯提示(錯誤位置飄紅)
  • 多種 SQL 的自動補全(智慧提示)

本文旨在講解上述功能的實現思路,對於技術細節,由於篇幅原因不會闡述的太詳細。

Monaco Languages

Monaco Editor 內建的 languages

Monaco Editor 內建了相當多的 languages,比如 javaScriptCSSShell 等。
Monaco Editor 依賴包的 ESM 入口檔案為 ./esm/vs/editor/editor.main.ts

而在這個檔案中,Monaco Editor 引入了所有內建的 Languages。

這裡 languages 檔案可以分為兩類,一類是../language資料夾下的,支援自動補全和飄紅提示功能;另一類則是../basic-languages資料夾下的,不支援自動補全功能和飄紅提示功能。

使用內建的 Language 功能

以使用 typescript 為例

import { editor } from 'monaco-editor';

const container = document.getElementById('container');

editor.create(container, {
    language: 'typescript'
})

此時我們會發現,我們的編輯器已經有語法高亮的功能了,但是瀏覽器控制檯會拋異常,另外也沒有自動補全功能和飄紅提示功能,

這其實是因為,Monaco Editor 無法載入到 language 對應的 worker,對應的解決辦法看這裡: Monaco integrate-esm
這裡我們使用 Using plain webpack的方式,首先將對應的 worker 檔案設定為 webpack entry

module.exports = {
    entry: {
        index: path.resolve( __dirname, './src/index.ts'),
        'editor.worker': 'monaco-editor/esm/vs/editor/editor.worker.js',
        'ts.worker': 'monaco-editor/esm/vs/language/typescript/ts.worker.js'
    },
}

另外還需要設定 Monaco Editor 的全域性環境變數,這主要是為了告訴 Monaco Editor 對應的 worker 檔案的路徑

import { editor } from 'monaco-editor';

(window as any).MonacoEnvironment = {
	getWorkerUrl: function (_moduleId, label) {
		switch (label) {
			case 'flink': {
				return './flink.worker.js';
			}
			case 'typescript': {
				return './ts.worker.js'
			}
			default: {
				return './editor.worker.js';
			}
		}
	}
};

const container = document.getElementById('container');

editor.create(container, {
    language: 'typescript'
})

這樣一個具有語法高亮自動補全飄紅提示 功能的 typescript 編輯器就設定好了

小結分析

首先上文中提到了當我們直接從 Monaco Editor 的入口檔案中匯入時,會自動的引入所有內建的 Languages,但是實際上這其中絕大都是我們不需要的,而由於其匯入方式,很顯然我們不需要的 languages 也無法被 treeShaking。要解決這個問題我們可以選擇從 monaco-editor/esm/vs/editor/editor.api 檔案中匯入Monaco Editor 核心 API,然後通過 monaco-editor-webpack-plugin 來按需匯入所需要的功能。另外這個外掛也可以自動處理Monaco Editor 內建的 worker 檔案的打包問題,以及自動注入 MonacoEnvironment全域性環境變數。

自定義 Language

註冊Language

Monaco Editor 提供了 monaco.languages.register方法,用來自定義 language

/**
 * Register information about a new language.
 */
export function register(language: ILanguageExtensionPoint): void;

export interface ILanguageExtensionPoint {
  id: string;
  extensions?: string[];
  filenames?: string[];
  filenamePatterns?: string[];
  firstLine?: string;
  aliases?: string[];
  mimetypes?: string[];
  configuration?: Uri;
}

第一步,我們需要註冊一個 language, 設定項中 id 對應的就是語言名稱(其他設定項可以暫時不填),這裡自定義的 language 名為 myLang

import { editor, languages } from 'monaco-editor';

languages.register({
    id: "myLang"
});

const container = document.getElementById('container');

editor.create(container, {
    language: 'myLang'
})

此時可以發現,頁面上的編輯器沒有任何其他附加功能,就是普通的文字編輯器。

設定 Language

通過 monaco.languages.setLanguageConfiguration,可以對 language 進行設定

/**
 * Set the editing configuration for a language.
 */
export function setLanguageConfiguration(
  languageId: string,
  configuration: LanguageConfiguration
): IDisposable;

/**
 * The language configuration interface defines the contract between extensions and
 * various editor features, like automatic bracket insertion, automatic indentation etc.
 */
export interface LanguageConfiguration {
    comments?: CommentRule;
    brackets?: CharacterPair[];
    wordPattern?: RegExp;
    indentationRules?: IndentationRule;
    onEnterRules?: OnEnterRule[];
    autoClosingPairs?: IAutoClosingPairConditional[];
    surroundingPairs?: IAutoClosingPair[];
    colorizedBracketPairs?: CharacterPair[];
    autoCloseBefore?: string;
    folding?: FoldingRules;
}

這些設定會影響 Monaco Editor 的一些預設行為,比如設定 autoClosingPairs中有一項為一對圓括號,那麼當輸入左圓括號後,會自動補全右圓括號。

import { languages } from "monaco-editor";
const conf: languages.LanguageConfiguration = {
  comments: {
    lineComment: "--",
    blockComment: ["/*", "*/"],
  },
  brackets: [
    ["(", ")"],
  ],
  autoClosingPairs: [
    { open: "(", close: ")" },
    { open: '"', close: '"' },
    { open: "'", close: "'" },
  ],
  surroundingPairs: [
    { open: "(", close: ")" },
    { open: '"', close: '"' },
    { open: "'", close: "'" },
  ],
};

languages.setLanguageConfiguration('myLang', conf)

高亮功能

Monarch

Moanco Editor 內建了 Monarch,用於實現語法高亮功能,它本質上是一個有限狀態機,我們可以通過JSON的形式來設定其狀態流轉邏輯,並通過monaco.languages.setMonarchTokensProvider API 應用該設定。關於Monarch 的具體用法可以看一下這篇文章 以及 Monarch Document
設定中最重要的是 tokenizer屬性,意思是分詞器,分詞器會自動對編輯器內部的文字進行分詞處理,每個分詞器都有一個 root state,在 root state 中可以有多條規則,規則內部可以參照其他 state。

下面是一個簡單的設定範例

import { languages } from "monaco-editor";
export const language: languages.IMonarchLanguage = {
	ignoreCase: true,
	tokenizer: {
		root: [
			{ include: '@comments' }, // 參照下面的 comments 規則
			{ include: '@whitespace' }, // 參照下面的 whiteSpace 規則
			{ include: '@strings' },// 參照下面的 strings 規則
		],
		whitespace: [[/\s+/, 'white']],
		comments: [
			[/--+.*/, 'comment'],
			[/\/\*/, { token: 'comment.quote', next: '@comment' }]
		],
		comment: [
			[/[^*/]+/, 'comment'],
			[/\*\//, { token: 'comment.quote', next: '@pop' }],
			[/./, 'comment']
		],
		strings: [
			[/'/, { token: 'string', next: '@string' }]
		],
		string: [
			[/[^']+/, 'string'],
			[/''/, 'string'],
			[/'/, { token: 'string', next: '@pop' }]
		],
	}
};

languages.setMonarchTokensProvider("myLang", language);

上面的設定中 root 下面有三條規則分別匹配 註釋(comments)字串(strings) 以及空白字元(whiteSpace), 每條規則可以大體分為兩部分:

  • 匹配方式,比如說正則
  • 對應的 token 型別(任意字串)

比如上述設定中 tokenizer.comments 規則

  comments: [
    [/--+.*/, 'comment'], // 左邊是正規表示式用來匹配文字,右邊是該規則對應的 token 名稱
    [/\/\*/, { token: 'comment.quote', next: '@comment' }] // 左邊是正規表示式用來匹配文字,右邊顯示宣告對應的 token 名稱
  ],

設定了如上 Monarch 之後,在編輯器內部輸入註釋或者字串,那麼Monaco editor 就會根據輸入的內容進行分詞處理

可以看到目前字串和註釋已經被高亮了。這裡有一個新的問題,不同型別的分詞的顏色是怎麼設定的

Monaco Theme

從上圖中右側的 Elements 面板中可以看到,不同型別的分詞,對應的標籤的 className 不同,它們是由 Monarch 設定中的 token 對映而來的。MonacoEditor 內建了一些 Theme,預設的 Theme 是 vs,而預設的 theme 中已經設定了上述 Monarch 中的 token 對應的顏色,所以我們應用上述設定後,對應的分詞直接就有了高亮顏色。
我們可以通過 monaco.editor.defineTheme 來定義一種新的 theme,如下例所示:

editor.defineTheme('myTheme', {
    base: 'vs',
    inherit: true,
    rules: [
        { token: 'comment', foreground: 'ff4400' },
        { token: 'string', foreground: '0000ff' }
    ],
    colors: {
    },
});

// xxxx

editor.create(container, {
  language: "myLang",
  theme: "myTheme"
});

這裡將註釋設定為紅色,字串設定為藍色,顯示效果如下圖所示

飄紅提示

飄紅提示的功能就是在程式碼錯誤的位置打上標記(一般是紅色波浪線),可以通過 monaco.editor.setModelMarkers API 來實現。比如我們想為 第1行的第1個字元到第2行的第2個字元 之間打上錯誤標記:

const editorIns = editor.create(container, {
  language: "myLang",
  theme: "myTheme",
  value: 
`hello
world`
});

const model = editorIns.getModel();

editor.setModelMarkers(model, 'myLang', [
	{
		startLineNumber: 1,
		startColumn: 1,
		endLineNumber: 2,
		endColumn: 2,
		message: "語法錯誤",
		severity: MarkerSeverity.Error
	}
])

severity 是標記型別,message 是提示資訊,效果如下所示。

到此為止,實現了飄紅的功能,但是沒有實現在語法錯誤處飄紅的功能,這需要額外的語法解析器支援,會在下文中講到。

自動補全功能

Monaco Editor 提供了 monaco.languages.registerCompletionItemProvider API 來實現自動補全功能

import { editor, languages, MarkerSeverity, Position, CancellationToken, Range  } from "monaco-editor";

languages.registerCompletionItemProvider('myLang', {
	triggerCharacters: ['.', '*'],
	provideCompletionItems(
		model: editor.IReadOnlyModel,
		position: Position,
		context: languages.CompletionContext,
		token: CancellationToken
	){
		const wordInfo = model.getWordUntilPosition(position);
        const wordRange = new Range(
            position.lineNumber,
            wordInfo.startColumn,
            position.lineNumber,
            wordInfo.endColumn
        );
    		return new Promise((resolve) => {
    			resolve({
    				suggestions: [
    					{
    						label: "SELECT",
    						kind: languages.CompletionItemKind.Keyword,
    						insertText: "SELECT",
    						range: wordRange,
    						detail: '關鍵字',
    					},
    					{
    						label: "SET",
    						kind: languages.CompletionItemKind.Keyword,
    						insertText: "SET",
    						range: wordRange,
    						detail: '關鍵字',
    					},
    					{
    						label: "SHOW",
    						kind: languages.CompletionItemKind.Keyword,
    						insertText: "SHOW",
    						range: wordRange,
    						detail: '關鍵字',
    					},
    				]
    			})
    		})
	}
})

registerCompletionItemProvider 接受兩個引數,第一個引數是 languageId 也就是 language 名稱,
第二個引數是一個 CompletionItemProviderCompletionItemProvidertriggerCharacters用來設定觸發自動補全的字元有哪些,而 provideCompletionItems則是一個函數,它接收 Monaco Editor 提供的當前的上下文資訊,返回自動補全項列表。如上例中返回了三個自動補全項,那麼當我們在編輯器中輸入 S時,就會出現設定的自動補全項候選選單。

通過這個 API 我們可以實現一種語言的關鍵字自動補全,只需要在CompletionItemProvider中返回該語言所有的關鍵字對應的自動補全項即可。
但是registerCompletionItemProvider目前做不到根據語意進行自動補全。
比如使用者寫一段 flinkSQL,當用戶輸入完 CREATE 關鍵字並按下空格後,應該出現的自動補全項應該是隻有TABLECATALOGDATABASEFUNCTIONVIEW
再比如當用戶輸入 SELECT * FROM 時,後面應該提示表名而不是其他無關的關鍵字。與上文中的飄紅提示一樣,這些語意資訊需要單獨的語法解析器來分析。

小結分析

到此為止,在**自定義 language **這一節中,我們已經瞭解了,在 Monaco Editor 中如何實現自定義語言的 語法高亮錯誤處飄紅提示自動補全
在數棧產品中,本節講到的功能都通過引入 monaco-sql-languages 依賴來實現,這是我們數棧 UED 團隊自研的開源專案,目前已經支援多種 SQL Languages。
由於目前為止沒有實現自定義 language 的語意分析功能,導致目前實現的編輯器不夠智慧。 另外,對於第一節中提到的 web worker ,在第二節中也沒有有提到,實際上 Monaco Editor 自帶的 web worker,也都是為了實現 language 的語意分析功能,下一節將闡述這一部分內容。

SQL Parser

要實現語意分析功能,很顯然我們需要一個語法解析器。除了基本的語法解析的基礎功能以外,我們還需要

  • 語法錯誤收集,收集編輯器中文字的語法錯誤資訊,用於錯誤飄紅提示功能。
  • 推斷文字中指定位置的候選項列表,對於編輯器來說,指定位置一般就是遊標所在位置。候選項是指在遊標所在的位置應該要寫什麼。比如 SQL 中 SELECT 關鍵字後面可以跟欄位或者函數,那麼我們所要實現的 sql parser 就應該提示出在 SELECT 關鍵字後面的候選項應該是欄位或者函數。

實現基礎的 SQL Parser

Antlr4 語法檔案

我們使用 Antlr4 來實現一個基本的 SQL Parser。Antlr4 是一個強大的解析器生成器,它能根據使用者自定義的語法檔案來生成對應的解析器。Antlr4 的語法檔案為 .g4檔案,內部可以包含多條規則,規則可以分為詞法規則和語法規則,詞法規則用於生成詞法分析器,語法規則用於生成語法解析器。
例,我們現在寫一份語法規則,匹配最簡單的 SELECT 語句(不包括子查詢、別名等規則),比如

SELECT * FROM table1;  -- eg1

SELECT table2.name, age FROM schema2.table2; -- eg2

那麼在antlr4中這份語法檔案應該這樣寫:

grammar SelectStatement;

/** 語法規則 begin */
program: selectStatement? EOF;

// 宣告 語句的匹配規則
selectStatement: KW_SELECT columnGroup KW_FROM tablePath SEMICOLON?;

// 宣告 語句中欄位部分的匹配規則,欄位部分可能為 col1, col2 的形式
columnGroup: columnPath (COMMA columnPath)*;

// 宣告 欄位名匹配規則,欄位名有可能為 db.table.col 或者 * 的形式
columnPath: dot_id | OP_STAR; 

// 宣告 表名匹配規則,表名有可能為 db.table 的形式
tablePath: dot_id; 

// 匹配 id.id 形式的識別符號號
dot_id: IDENTIFIER_LITERAL (DOT IDENTIFIER_LITERAL)*; 
/** 語法規則 end */ 


/** 詞法規則 begin */
KW_SELECT:          'SELECT'; // 匹配 SELECT 關鍵字
KW_FROM:            'FROM'; // 匹配 FROM 關鍵字
OP_STAR:            '*'; // 匹配 * 
DOT:                '.'; // 匹配 .
COMMA:              ','; // 匹配 ,
SEMICOLON:          ';'; // 匹配 ;
IDENTIFIER_LITERAL: [A-Z_a-z][A-Z_0-9a-z]*; // 匹配識別符號

WS:                 [ \t\n\r]+ -> skip ; // 忽略空格換行等空白字元
/** 詞法規則 end */

語法規則的編寫格式類似於 EBNF
然後執行 antlr4 命令,根據所寫的語法檔案生成對應的解析器。可以直接使用官方檔案中提供的方式 antlr4 typescript-target doc ,或者直接使用社群提供的 antlr4ts 包,這裡以使用 antlr4ts 為例。
生成的檔案結果如下所示:

使用 Antlr4 生成的 Parser

在使用Antlr4 的生成的 Parser 之前我們需要安裝,Antlr4 的執行時包。你可以將 Antlr4 的執行時包通過語法檔案生成的parser檔案之間的關係,類比為 react 和 react-dom之間的關係。這裡以使用 antlr4ts 為執行時

import { CommonTokenStream, CharStreams } from 'antlr4ts';
import { SelectStatementLexer } from '../lib/selectStatement/SelectStatementLexer';
import { SelectStatementParser } from '../lib/selectStatement/SelectStatementParser';

class SelectParser {
  private createLexer(input: string) {
    const inputStream = CharStreams.fromString(input);
    const lexer = new SelectStatementLexer(inputStream);
    return lexer
  }

  private createParser (input: string) {
    const lexer = this.createLexer(input);
    const tokens = new CommonTokenStream(lexer);
    const parser = new SelectStatementParser(tokens);
    return parser
  }

  parse (sql: string) {
    const parser = this.createParser(sql)
    const parseTree = parser.selectStatement();
    return parseTree;
  }
}
// 試一下效果
const selectParser = new SelectParser();
const parseTree = selectParser.parse('SELECT * FROM table1');

獲取文字中的錯誤資訊

當解析一個含有錯誤的文字時,Antlr4 會輸出錯誤資訊,例如輸入

selectParser.parse('SELECT id FRO');

控制檯列印

可以看到錯誤資訊中包含了文字中的錯誤所處的位置,我們可以通過使用 Antlr4 ParserErrorListener 來獲取錯誤資訊。

宣告一個 ParserErrorListener

import { ParserErrorListener } from 'antlr4ts';

export class SelectErrorListener implements ParserErrorListener {
    private _parserErrorSet: Set<any> = new Set();

    syntaxError(_rec,_ofSym, line, charPosInLine,msg) {
        let endCol = charPosInLine + 1;
        this._parserErrorSet.add({
            startLine: line,
            endLine: line,
            startCol: charPosInLine,
            endCol: endCol,
            message: msg,
        })
    }

    clear () {
        this._parserErrorSet.clear();
    }

    get parserErrors () {
        return Array.from(this._parserErrorSet) 
    }
}

使用 ParserErrorListener 收集錯誤資訊

import { CommonTokenStream, CharStreams } from 'antlr4ts';
import { SelectStatementLexer } from '../lib/selectStatement/SelectStatementLexer';
import { SelectStatementParser } from '../lib/selectStatement/SelectStatementParser';


class SelectParser {
    private _errorListener = new SelectErrorListener();

    createLexer(input: string) {
        const inputStream = CharStreams.fromString(input);
        const lexer = new SelectStatementLexer(inputStream);
        this._errorListener.clear();
        lexer.removeErrorListeners(); // 移除 Antlr4 內建的 ErrorListener
        lexer.addErrorListener(this._errorListener)
        return lexer
    }

    createParser (input: string) {
        const lexer = this.createLexer(input);
        const tokens = new CommonTokenStream(lexer);
        const parser = new SelectStatementParser(tokens);
        parser.removeErrorListeners(); // 移除 Antlr4 內建的 ErrorListener
        parser.addErrorListener(this._errorListener);
        return parser
    }

    parse (sql: string) {
        const parser = this.createParser(sql)
        const parseTree = parser.selectStatement();
        console.log(this._errorListener.parserErrors);
        return {
          parseTree,
          errors: this._errorListener.parserErrors,
        };
    }
}
// 試一下效果
const selectParser = new SelectParser();
const { errors } = selectParser.parse('SELECT id FRO');
console.log(errors);

列印結果

這樣我們就獲取到了文字中的語法錯誤出現的位置,以及錯誤資訊。
到此為止上文中遺留的第一個問題就已經差不多解決了,我們只需要在合適的時機將編輯器的內容進行解析,拿到錯誤資訊並且通過 editor.setModelMarkers這個 API 讓錯誤的位置飄紅就大功告成了。

自動補全功能

對於自動補全功能,Antlr4 並沒有直接提供,但是社群已經有了比較優秀的解決方案 - antlr-c3 。它的作用是根據Antlr4 Parser 的解析結果,分析指定位置填哪些詞法/語法規則是合法的
antlr4-c3 的使用方式比較簡單。

import { CodeCompletionCore } from "antlr4-c3";

// 這裡 parser 是 parser 範例
let core = new CodeCompletionCore(parser); 
// tokenIndex 是想要自動補全的位置,對應由編輯器的遊標位置轉換而來
// parserContext 則是解析完之後的返回的 ParserTree 或者 ParserTree 的子節點(傳入子節點可以更高效)
let candidates = core.collectCandidates(tokenIndex, parserContext);

那麼結合上文中寫的 SelectParser,程式碼應該是這樣

import { CodeCompletionCore } from "antlr4-c3";
import { SelectParser } from "./selectParser";

/**
 * input 源文字
 * caretPosition 編輯器遊標位置
 */
function getSuggestions(input: string, caretPosition) {
    const selectParser = new SelectParser();
    const parserIns = selectParser.createParser(input)
    let core = new CodeCompletionCore(parserIns);

    const parserContext = parserIns.selectStatement();
    // 虛擬碼
    const tokenIndex = caretPosition2TokenIndex(caretPosition)

    let candidates = core.collectCandidates(tokenIndex, parserContext);
}

core.collectCandidates 的返回值的資料型別如下

interface CandidatesCollection {
    tokens: Map<number, TokenList>;
    rules: Map<number, CandidateRule>;
}

tokens 對應的是詞法規則提示,比如關鍵字等,rules 對應的是語法規則,比如上述語法檔案中的 columnPathtablePath等。
需要注意的是,antlr4-c3 預設不收集語法規則,需要我們手動設定需要收集的語法規則

import { SelectStatementParser } from '../lib/selectStatement/SelectStatementParser';


let core = new CodeCompletionCore(parserIns);

core.preferredRules= new Set([
    SelectStatementParser.RULE_tablePath,
    SelectStatementParser.RULE_columnPath
])
// 設定需要收集 tablePath 和 columnPath

這樣我們就收集到了在指定位置的可以填什麼。接下來我們需要將結果進行轉換成我們需要的資料結果

import { CodeCompletionCore } from "antlr4-c3";
import { SelectParser } from "./selectParser";
import { SelectStatementParser } from '../lib/selectStatement/SelectStatementParser';

/**
 * input 源文字
 * caretPosition 編輯器遊標位置
 */
export function getSuggestions(input: string, caretPosition?: any) {
    const selectParser = new SelectParser();
    const parserIns = selectParser.createParser(input)
    let core = new CodeCompletionCore(parserIns);

    core.preferredRules= new Set([
        SelectStatementParser.RULE_tablePath,
        SelectStatementParser.RULE_columnPath
    ])

    const parserContext = parserIns.selectStatement();
    const tokenIndex = caretPosition2TokenIndex(caretPosition);

    let candidates = core.collectCandidates(tokenIndex, parserContext);

    const rule = [];
    const keywords = []

    for (let candidate of candidates.rules) {
        const [ruleType] = candidate;
        let syntaxContextType;
        switch (ruleType) {
            case SelectStatementParser.RULE_tablePath: {
                syntaxContextType = 'table';
                break;
            }
            case SelectStatementParser.RULE_columnPath: {
                syntaxContextType = 'column';
                break;
            }
            default:
                break;
        }
        if (syntaxContextType) {
            rule.push(syntaxContextType)
        }
    }

    for (let candidate of candidates.tokens) {
        const symbolicName = parserIns.vocabulary.getSymbolicName(candidate[0]);
        const displayName = parserIns.vocabulary.getDisplayName(candidate[0]);
        if(symbolicName && symbolicName.startsWith('KW_')) {
            const keyword = displayName.startsWith("'") && displayName.endsWith("'")
                ? displayName.slice(1, -1)
                : displayName
            keywords.push(keyword);
        }
    }

    console.log('===== suggest keywords: ',keywords);
    console.log('===== suggest rules:', rule);
}

這樣我們就拿到了要提示的關鍵字和語法規則。關鍵字可以直接用於生成自動補全項,語法規則可以用於提示表名、欄位名等。

小結分析

在這一節中,我們已經瞭解了,如何使用 Antlr4 和 antlr4-c3 來實現更加智慧的飄紅提示以及自動補全功能。
這一部分功能,在 monaco-sql-languages 中通過引入數棧前端團隊自研的開源專案 dt-sql-parser 實現。
前文中提到的 worker 檔案也正是用於執行 sql parser,因為dt-sql-parser 的解析可能會比較耗時,為了避免用項使用者互動,將 sql parser 放到 web worker 中執行顯然是更明智的選擇。

總結

總的來說

  • 多種 SQL 的語法高亮
  • 多種 SQL 的報錯提示(錯誤位置飄紅)
  • 多種 SQL 的自動補全(智慧提示)

三個功能大部分都可以通過 MonacoEditor 內建的 API 來實現,只是關鍵的語法解析功能需要使用 Antlr4 實現。整體上來說大部分的工作在編寫 Antlr4 的語法檔案以及方案整合上面。

Github 連結

最後

歡迎關注【袋鼠雲數棧UED團隊】~
袋鼠雲數棧UED團隊持續為廣大開發者分享技術成果,相繼參與開源了歡迎star