數(shù)棧產品中的代碼編譯器

前言

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

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

本文旨在講解上述功能的實現(xiàn)思路幔妨,對于技術細節(jié)鹦赎,由于篇幅原因不會闡述的太詳細。

Monaco Languages

Monaco Editor 內置的 languages

Monaco Editor 內置了相當多的 languages误堡,比如 javaScript古话、CSSShell 等埂伦。<br />Monaco Editor 依賴包的 ESM 入口文件為 ./esm/vs/editor/editor.main.ts<br />

file
<br />而在這個文件中,Monaco Editor 引入了所有內置的 Languages思恐。<br />
file
<br />這里 languages 文件可以分為兩類沾谜,一類是../language文件夾下的,支持自動補全和飄紅提示功能胀莹;另一類則是../basic-languages文件夾下的基跑,不支持自動補全功能和飄紅提示功能。

使用內置的 Language 功能

以使用 typescript 為例

import { editor } from 'monaco-editor';

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

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

此時我們會發(fā)現(xiàn)描焰,我們的編輯器已經有語法高亮的功能了媳否,但是瀏覽器控制臺會拋異常,另外也沒有自動補全功能和飄紅提示功能荆秦,<br />

file
<br />這其實是因為篱竭,Monaco Editor 無法加載到 language 對應的 worker,對應的解決辦法看這里: Monaco integrate-esm步绸。<br />這里我們使用 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 的全局環(huán)境變量,這主要是為了告訴 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 編輯器就設置好了<br />

file

小結分析

首先上文中提到了當我們直接從 Monaco Editor 的入口文件中導入時,會自動的引入所有內置的 Languages刑桑,但是實際上這其中絕大都是我們不需要的氯质,而由于其導入方式,很顯然我們不需要的 languages 也無法被 treeShaking祠斧。要解決這個問題我們可以選擇從 monaco-editor/esm/vs/editor/editor.api 文件中導入Monaco Editor 核心 API闻察,然后通過 monaco-editor-webpack-plugin 來按需導入所需要的功能。另外這個插件也可以自動處理Monaco Editor 內置的 worker 文件的打包問題,以及自動注入 MonacoEnvironment全局環(huán)境變量蜓陌。

自定義 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'
})

此時可以發(fā)現(xiàn),頁面上的編輯器沒有任何其他附加功能隧期,就是普通的文本編輯器飒责。

設置 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拾并,用于實現(xiàn)語法高亮功能,它本質上是一個有限狀態(tài)機鹏浅,我們可以通過JSON的形式來配置其狀態(tài)流轉邏輯嗅义,并通過monaco.languages.setMonarchTokensProvider API 應用該配置。關于Monarch 的具體用法可以看一下這篇文章 以及 Monarch Document隐砸。<br />配置中最重要的是 tokenizer屬性之碗,意思是分詞器,分詞器會自動對編輯器內部的文本進行分詞處理季希,每個分詞器都有一個 root state褪那,在 root state 中可以有多條規(guī)則,規(guī)則內部可以引用其他 state式塌。

下面是一個簡單的配置示例

import { languages } from "monaco-editor";
export const language: languages.IMonarchLanguage = {
    ignoreCase: true,
    tokenizer: {
        root: [
            { include: '@comments' }, // 引用下面的 comments 規(guī)則
            { include: '@whitespace' }, // 引用下面的 whiteSpace 規(guī)則
            { include: '@strings' },// 引用下面的 strings 規(guī)則
        ],
        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 下面有三條規(guī)則分別匹配 注釋(comments)博敬、字符串(strings) 以及空白字符(whiteSpace), 每條規(guī)則可以大體分為兩部分:

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

比如上述配置中 tokenizer.comments 規(guī)則

  comments: [
    [/--+.*/, 'comment'], // 左邊是正則表達式用來匹配文本冶忱,右邊是該規(guī)則對應的 token 名稱
    [/\/\*/, { token: 'comment.quote', next: '@comment' }] // 左邊是正則表達式用來匹配文本,右邊顯示聲明對應的 token 名稱
  ],

配置了如上 Monarch 之后境析,在編輯器內部輸入注釋或者字符串囚枪,那么Monaco editor 就會根據(jù)輸入的內容進行分詞處理<br />
file

可以看到目前字符串和注釋已經被高亮了。這里有一個新的問題劳淆,不同類型的分詞的顏色是怎么設置的链沼?

Monaco Theme

從上圖中右側的 Elements 面板中可以看到,不同類型的分詞沛鸵,對應的標簽的 className 不同括勺,它們是由 Monarch 配置中的 token 映射而來的缆八。MonacoEditor 內置了一些 Theme,默認的 Theme 是 vs疾捍,而默認的 theme 中已經設置了上述 Monarch 中的 token 對應的顏色奈辰,所以我們應用上述配置后,對應的分詞直接就有了高亮顏色乱豆。<br />我們可以通過 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"
});

這里將注釋設置為紅色,字符串設置為藍色宛裕,顯示效果如下圖所示<br />
file

飄紅提示

飄紅提示的功能就是在代碼錯誤的位置打上標記(一般是紅色波浪線)瑟啃,可以通過 monaco.editor.setModelMarkers API 來實現(xiàn)。比如我們想為 第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 是提示信息蛹屿,效果如下所示。<br />

file
<br />到此為止岩榆,實現(xiàn)了飄紅的功能错负,但是沒有實現(xiàn)在語法錯誤處飄紅的功能,這需要額外的語法解析器支持勇边,會在下文中講到犹撒。

自動補全功能

Monaco Editor 提供了 monaco.languages.registerCompletionItemProvider API 來實現(xiàn)自動補全功能

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 接受兩個參數(shù),第一個參數(shù)是 languageId 也就是 language 名稱粥诫,<br />第二個參數(shù)是一個 CompletionItemProvider油航,CompletionItemProvidertriggerCharacters用來配置觸發(fā)自動補全的字符有哪些崭庸,而 provideCompletionItems則是一個函數(shù)怀浆,它接收 Monaco Editor 提供的當前的上下文信息,返回自動補全項列表怕享。如上例中返回了三個自動補全項执赡,那么當我們在編輯器中輸入 S時,就會出現(xiàn)配置的自動補全項候選菜單函筋。<br />

file
<br />通過這個 API 我們可以實現(xiàn)一種語言的關鍵字自動補全沙合,只需要在CompletionItemProvider中返回該語言所有的關鍵字對應的自動補全項即可。<br />但是registerCompletionItemProvider目前做不到根據(jù)語義進行自動補全跌帐。<br />比如用戶寫一段 flinkSQL首懈,當用戶輸入完 CREATE 關鍵字并按下空格后,應該出現(xiàn)的自動補全項應該是只有TABLE谨敛、CATALOG究履、DATABASEFUNCTION脸狸、 VIEW最仑。<br />再比如當用戶輸入 SELECT * FROM 時藐俺,后面應該提示表名而不是其他無關的關鍵字。與上文中的飄紅提示一樣泥彤,這些語義信息需要單獨的語法解析器來分析欲芹。

小結分析

到此為止,在**自定義 language **這一節(jié)中吟吝,我們已經了解了菱父,在 Monaco Editor 中如何實現(xiàn)自定義語言的 語法高亮錯誤處飄紅提示爸黄、自動補全滞伟。<br />在數(shù)棧產品中,本節(jié)講到的功能都通過引入 monaco-sql-languages 依賴來實現(xiàn)炕贵,這是我們數(shù)棧 UED 團隊自研的開源項目梆奈,目前已經支持多種 SQL Languages。<br />由于目前為止沒有實現(xiàn)自定義 language 的語義分析功能称开,導致目前實現(xiàn)的編輯器不夠智能亩钟。 另外,對于第一節(jié)中提到的 web worker 鳖轰,在第二節(jié)中也沒有有提到清酥,實際上 Monaco Editor 自帶的 web worker,也都是為了實現(xiàn) language 的語義分析功能蕴侣,下一節(jié)將闡述這一部分內容焰轻。

SQL Parser

要實現(xiàn)語義分析功能,很顯然我們需要一個語法解析器昆雀。除了基本的語法解析的基礎功能以外辱志,我們還需要

  • 語法錯誤收集,收集編輯器中文本的語法錯誤信息狞膘,用于錯誤飄紅提示功能揩懒。
  • 推斷文本中指定位置的候選項列表,對于編輯器來說挽封,指定位置一般就是光標所在位置已球。候選項是指在光標所在的位置應該要寫什么。比如 SQL 中 SELECT 關鍵字后面可以跟字段或者函數(shù)辅愿,那么我們所要實現(xiàn)的 sql parser 就應該提示出在 SELECT 關鍵字后面的候選項應該是字段或者函數(shù)智亮。

實現(xiàn)基礎的 SQL Parser

Antlr4 語法文件

我們使用 Antlr4 來實現(xiàn)一個基本的 SQL Parser。Antlr4 是一個強大的解析器生成器点待,它能根據(jù)用戶自定義的語法文件來生成對應的解析器阔蛉。Antlr4 的語法文件為 .g4文件,內部可以包含多條規(guī)則亦鳞,規(guī)則可以分為詞法規(guī)則和語法規(guī)則馍忽,詞法規(guī)則用于生成詞法分析器棒坏,語法規(guī)則用于生成語法解析器。<br />例遭笋,我們現(xiàn)在寫一份語法規(guī)則坝冕,匹配最簡單的 SELECT 語句(不包括子查詢、別名等規(guī)則)瓦呼,比如

SELECT * FROM table1;  -- eg1

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

那么在antlr4中這份語法文件應該這樣寫:

grammar SelectStatement;

/** 語法規(guī)則 begin */
program: selectStatement? EOF;

// 聲明 語句的匹配規(guī)則
selectStatement: KW_SELECT columnGroup KW_FROM tablePath SEMICOLON?;

// 聲明 語句中字段部分的匹配規(guī)則喂窟,字段部分可能為 col1, col2 的形式
columnGroup: columnPath (COMMA columnPath)*;

// 聲明 字段名匹配規(guī)則,字段名有可能為 db.table.col 或者 * 的形式
columnPath: dot_id | OP_STAR; 

// 聲明 表名匹配規(guī)則央串,表名有可能為 db.table 的形式
tablePath: dot_id; 

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


/** 詞法規(guī)則 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 ; // 忽略空格換行等空白字符
/** 詞法規(guī)則 end */

語法規(guī)則的編寫格式類似于 EBNF磨澡。<br />然后運行 antlr4 命令,根據(jù)所寫的語法文件生成對應的解析器质和∥壬悖可以直接使用官方文檔中提供的方式 antlr4 typescript-target doc ,或者直接使用社區(qū)提供的 antlr4ts 包饲宿,這里以使用 antlr4ts 為例厦酬。<br />生成的文件結果如下所示:<br />

file

使用 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');

控制臺打印<br />
file

<br />可以看到錯誤信息中包含了文本中的錯誤所處的位置,我們可以通過使用 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);

打印結果<br />

file
<br />這樣我們就獲取到了文本中的語法錯誤出現(xiàn)的位置筹裕,以及錯誤信息。<br />到此為止上文中遺留的第一個問題就已經差不多解決了礼搁,我們只需要在合適的時機將編輯器的內容進行解析饶碘,拿到錯誤信息并且通過 editor.setModelMarkers這個 API 讓錯誤的位置飄紅就大功告成了目尖。

自動補全功能

對于自動補全功能馒吴,Antlr4 并沒有直接提供,但是社區(qū)已經有了比較優(yōu)秀的解決方案 - antlr-c3 瑟曲。它的作用是根據(jù)Antlr4 Parser 的解析結果饮戳,分析指定位置填哪些詞法/語法規(guī)則是合法的。<br />antlr4-c3 的使用方式比較簡單洞拨。

import { CodeCompletionCore } from "antlr4-c3";

// 這里 parser 是 parser 實例
let core = new CodeCompletionCore(parser); 
// tokenIndex 是想要自動補全的位置扯罐,對應由編輯器的光標位置轉換而來
// parserContext 則是解析完之后的返回的 ParserTree 或者 ParserTree 的子節(jié)點(傳入子節(jié)點可以更高效)
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 的返回值的數(shù)據(jù)類型如下

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

tokens 對應的是詞法規(guī)則提示烦衣,比如關鍵字等歹河,rules 對應的是語法規(guī)則掩浙,比如上述語法文件中的 columnPathtablePath等。<br />需要注意的是秸歧,antlr4-c3 默認不收集語法規(guī)則厨姚,需要我們手動設置需要收集的語法規(guī)則

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


let core = new CodeCompletionCore(parserIns);

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

這樣我們就收集到了在指定位置的可以填什么。接下來我們需要將結果進行轉換成我們需要的數(shù)據(jù)結果

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);
}

這樣我們就拿到了要提示的關鍵字和語法規(guī)則键菱。關鍵字可以直接用于生成自動補全項谬墙,語法規(guī)則可以用于提示表名、字段名等经备。

小結分析

在這一節(jié)中拭抬,我們已經了解了,如何使用 Antlr4 和 antlr4-c3 來實現(xiàn)更加智能的飄紅提示以及自動補全功能侵蒙。<br />這一部分功能造虎,在 monaco-sql-languages 中通過引入數(shù)棧前端團隊自研的開源項目 dt-sql-parser 實現(xiàn)。<br />前文中提到的 worker 文件也正是用于運行 sql parser纷闺,因為dt-sql-parser 的解析可能會比較耗時累奈,為了避免用項用戶交互,將 sql parser 放到 web worker 中運行顯然是更明智的選擇急但。

總結

總的來說

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

三個功能大部分都可以通過 MonacoEditor 內置的 API 來實現(xiàn)澎媒,只是關鍵的語法解析功能需要使用 Antlr4 實現(xiàn)。整體上來說大部分的工作在編寫 Antlr4 的語法文件以及方案整合上面波桩。

Github 鏈接

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末戒努,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子镐躲,更是在濱河造成了極大的恐慌储玫,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件萤皂,死亡現(xiàn)場離奇詭異撒穷,居然都是意外死亡,警方通過查閱死者的電腦和手機裆熙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門端礼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人入录,你說我怎么就攤上這事蛤奥。” “怎么了僚稿?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵凡桥,是天一觀的道長。 經常有香客問我蚀同,道長缅刽,這世上最難降的妖魔是什么啊掏? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮衰猛,結果婚禮上脖律,老公的妹妹穿的比我還像新娘。我一直安慰自己腕侄,他們只是感情好小泉,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著冕杠,像睡著了一般微姊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上分预,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天兢交,我揣著相機與錄音,去河邊找鬼笼痹。 笑死配喳,一個胖子當著我的面吹牛,可吹牛的內容都是我干的凳干。 我是一名探鬼主播晴裹,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼救赐!你這毒婦竟也來了涧团?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤经磅,失蹤者是張志新(化名)和其女友劉穎泌绣,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體预厌,經...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡阿迈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了轧叽。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片苗沧。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖犹芹,靈堂內的尸體忽然破棺而出崎页,到底是詐尸還是另有隱情鞠绰,我是刑警寧澤腰埂,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站蜈膨,受9級特大地震影響屿笼,放射性物質發(fā)生泄漏牺荠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一驴一、第九天 我趴在偏房一處隱蔽的房頂上張望休雌。 院中可真熱鬧,春花似錦肝断、人聲如沸杈曲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽担扑。三九已至,卻和暖如春趣钱,著一層夾襖步出監(jiān)牢的瞬間涌献,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工首有, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留燕垃,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓井联,卻偏偏與公主長得像卜壕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子烙常,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內容