編程語言的 IDE 支持
或許是出自于對編寫編程語言的興趣,又或許是對于創(chuàng)建 IDE/編輯器的興趣,對于『IDE/編輯器是如何提供編程語言的支持』,我充滿了興趣。其中的一個主要原因是,這是每天我們打交道最多的工具,另外一個原因可能是,咦,我們怎么沒有國產(chǎn)的 IDE(手動狗頭)。
編輯器 & IDE
先前,我已經(jīng)在那篇《編輯器的自制》中介紹了,怎么去創(chuàng)建一個簡單的文本編輯器?這是一個相對簡單的問題。對于一個可用的代碼編輯器來說,我們對它的基本訴求是:快速啟動 + 語法高亮,然后能進行基本的文本編輯。不過呢,這是以我角度來看待問題的,我的想法里:一個編輯器,就干好一個編輯器應該做的事情。對于一些開發(fā)人員而言,他/她們會配置上強大的各種支持功能,以使它看上去像是一個 IDE。而后呢,它失去了快速啟動的能力,或者失去了一部分的快速啟動的速度,這便是有些遺憾的。
關(guān)于編輯器與 IDE 的這一一點的討論,似乎會有些偏頗。我自知我是一個 IDE 黨,擁有公司提供的 Jetbrains 全家桶。日常我也會使用 Sublime Text、Xi Editor、Vim、VS Code 進行一些快速的文件修改和查找。順便提一句,盡管過去我是一個 Emacs 粉,但是自我寫了自己的 Markdown 編輯器之后,我已經(jīng)……。好在下一步,我打算做一個自己的代碼編輯器,這樣一來,也許就不會那么內(nèi)疚了?;蛟S呢,我已經(jīng)在實現(xiàn)的路上了。
回到正題上,如果是一個 IDE 的話(以 IDEA 老用戶的感受),那么我估摸著需要這么一些功能:
- 語法高亮
- 文本編輯
- 子系統(tǒng)關(guān)聯(lián)與集成
- 跳轉(zhuǎn)與引用分析
- 智能感知
- 重構(gòu)
- 快速修復
- 語言特性分析
- 結(jié)構(gòu)化視圖
- ……
PS:仔細一看,諸如于 VSCode 這一類強大的編輯器里,已經(jīng)內(nèi)置了大部分的功能,而且它還是免費的。你還只需要一個,不需要啟動多個不同的 IDE,還省下了硬盤空間。笑~
不過,總的來說,這些功能都依賴于詞法分析,有了這個支持,才能進行其它部分的操作。
語法分析
對于開發(fā)工具來說,語法分析有幾個重要的功能:
- 語法高亮,是指根據(jù)術(shù)語類別來顯示不同的顏色與字體以增強可讀性的一種編輯器特性。
- 實現(xiàn)智能感知
- 實現(xiàn)跳轉(zhuǎn)和引用分析
從我粗糙的調(diào)查來看,大致可以分析為四類:
- 基于正則表達式來實現(xiàn)語法分析
Sublime Text 基于 YAML 形式的正則匹配方式:Sublime Syntax files
Textmate、VS Code 基于 JSON 的正則匹配方式:Language Grammars
- 基于語法分析器(如 BNF)生成中間代碼
Jetbrins 基于 BNF 生成代碼的方式:Grammar and Parser
- 自制 DSL 進行語法解析
Vim 基于正則 + 自制 DSL:Vim documentation: syntax、Rust 示例
- 手寫解析語法
Eclipse IDE 提供了個 JFace editor,但是似乎是要手寫:FAQ How do I provide syntax coloring in an editor?
Emacs Mode: ModeTutorial
每一類各自有各自的優(yōu)缺點和編寫難度。但是,總的來說,沒有一個方式是簡單的。
正則實現(xiàn)語法分析
對于正則方式來說,不論是 Sublime Text 還是 Textmate 及基于 Textmate 語法規(guī)則的 VS Code,它們都有一個顯著的缺點:長,如 VCode 的java.tmLanguage.json,從長度上來說,我看到的這個版本有 1831 行。表達方式也有些繁瑣:
- "comments": {
- "patterns": [
- {
- "captures": {
- "0": {
- "name": "punctuation.definition.comment.java"
- }
- },
- "match": "/\\*\\*/",
- "name": "comment.block.empty.java"
- },
- {
- "include": "#comments-inline"
- }
- ]
- },
其中還有各種 include 關(guān)系等。對于 Sublime Text 也是類似的:
- comments:
- - match: /\*\*/
- scope: comment.block.empty.java punctuation.definition.comment.java
- - include: scope:text.html.javadoc
- - include: comments-inline
看了看,是不是會懷疑他們建立了語法同盟。
但是呢,yaml 和 json 是一個編程語言無關(guān)的東西。所以,VS Code 和 Atom 可以基于 Textmate 語法規(guī)則,快速建立對于主流語言的詞法分析,從而建立了語法高亮的支持。
我們也可以說 BNF 是一種編程語言無關(guān)的東西。但是,實際上在我們操作的時候,就會加入一些編程語言特定的要素。
語法分析器分析
由于先前編寫系統(tǒng)分析工具 Coca 和通用語法分析器 Chapi ,我對于 BNF 的詞法也是頗為上手的——實際上不難。唯一麻煩的地方就是,寫完之后,我們要編寫代碼做一些轉(zhuǎn)換,所以讓我們來看看 Jetbrians 插件的示例:
- COMMENT = 'regexp://[^\r\n]*'
- BLOCK_COMMENT = 'regexp:[/][*][^*]*[*]+([^/*][^*]*[*]+)*[/]'
這一點上和 antlr 沒有太大的區(qū)別:
- WS: [ \t\r\n\u000C]+ -> channel(HIDDEN);
- COMMENT: '/*' .*? '*/' -> channel(HIDDEN);
- LINE_COMMENT: '//' ~[\r\n]* -> channel(HIDDEN);
然后,就是設(shè)計和分析詞法了:
- functionParameters ::=
- LPAREN inputParameters RPAREN outputParameters?
- | IN SUB GT inputParameters
- | outputParameters
接著,在 IDEA 里面,我們可以通過這個 BNF 文件生成對應的 Lexer 文件和代碼等。對于使用 Antlr 編寫的詞法來說,Java 部分的代碼規(guī)模也就在 800 左右。
不過呢,從兩者的閱讀體驗對比來看,顯然 BNF 會更加友好一點。
自制 DSL 語法解析
頗為遺憾的是,我尚未寫過任何的 Vim 插件,好在我還知道 Vim 是如何退出來的。我使用 Vim 作為 git 的 editor,還熟知一些 Vim 編輯的常用快捷鍵。所以,語法高亮這一部分主要是參考 Vim 的文檔編寫和代碼示例。這里我找到了一個不錯的中文翻譯:語法高亮
總的來說,語法規(guī)則就是: syn vim關(guān)鍵字 匹配規(guī)則,如:
- syn region rustCommentLine start="//" end="$" contains=rustTodo,@Spell
- syn region rustCommentLineDoc start="//\%(//\@!\|!\)" end="$" contains=rustTodo,@Spell
- syn region rustCommentLineDocError start="//\%(//\@!\|!\)" end="$" contains=rustTodo,@Spell contained
- syn region rustCommentBlock matchgroup=rustCommentBlock start="/\*\%(!\|\*[*/]\@!\)\@!" end="\*/" contains=rustTodo,
看上去依舊是正則匹配,如 Float:
- syn match rustFloat display "\<[0-9][0-9_]*\%(\.[0-9][0-9_]*\)\%([eE][+-]\=[0-9_]\+\)\=\(f32\|f64\)\="
- syn match rustFloat display "\<[0-9][0-9_]*\%(\.[0-9][0-9_]*\)\=\%([eE][+-]\=[0-9_]\+\)\(f32\|f64\)\="
- syn match rustFloat display "\<[0-9][0-9_]*\%(\.[0-9][0-9_]*\)\=\%([eE][+-]\=[0-9_]\+\)\=\(f32\|f64\)"
不過,從算法形式上來說,完勝 Textmate 和 Sublime,畢竟是高級的 DSL。
編程語言語法解析
Emacs 的 mode 里包含了對于語法高亮的處理,于是為了這個高亮,我們需要寫寫 emacs lisp 代碼。如:
- (defvar rust-formatting-macro-opening-re
- "[[:space:]\n]*[({[][[:space:]\n]*"
- "Regular expression to match the opening delimiter of a Rust formatting macro.")
- (defvar rust-start-of-string-re
- "\\(?:r#*\\)?\""
- "Regular expression to match the start of a Rust raw string.")
對于 Eclipse 來說,這個過程就更加麻煩了。
語言的高級支持
在我們實現(xiàn)了開發(fā)工具的詞法分析接口之后,我們就能按不同的 IDE/編輯器所定義的接口,進行定制了。這是一個繁雜,而又充滿挑戰(zhàn)的工作。對于不同的工具來說,它們的接口相關(guān)也甚多。我也并非都能一一了解 API,所以只能簡單的以 IDEA 作為一個示例來展示。主要原因大概有兩個:1. 我日常使用的是 Jetbrains 相關(guān)的 IDE;2. 我已經(jīng)有一部分代碼了。
語法高亮
在進行了復雜的語法分析之后,接著,我們就可以快速進入一個簡單的環(huán)節(jié),對代碼進行高亮。關(guān)于高亮的話,我們可以快速進行一個分類:
- 關(guān)鍵詞。即編程語言的關(guān)鍵詞,如 C 語言中的 32 個關(guān)鍵詞。
- 標識符。用戶定義的字符串,如變量名、結(jié)構(gòu)體名、函數(shù)名等等。
- 特殊詞法。
- 重要的詞法。根據(jù)需要,可以針對于函數(shù)名、靜態(tài)函數(shù)名等進行標識,以提升識別度。
如下是 Go 語言的一些關(guān)鍵詞:
- (defconst go-mode-keywords
- '("break" "default" "func" "interface" "select"
- "case" "defer" "go" "map" "struct"
- "chan" "else" "goto" "package" "switch"
- "const" "fallthrough" "if" "range" "type"
- "continue" "for" "import" "return" "var")
- "All keywords in the Go language. Used for font locking.")
所以,在這個場景之下,不論是何種的 IDE 又或者是編輯器都可以快速實現(xiàn)。
跳轉(zhuǎn) goto
不同開發(fā)工具,有各種的跳轉(zhuǎn)規(guī)則,不同的語言也有各自的跳轉(zhuǎn)方式。如 Emacs 的 go-mode 就定義了一系列的跳轉(zhuǎn):
- (let ((m (define-prefix-command 'go-goto-map)))
- (define-key m "a" #'go-goto-arguments)
- (define-key m "d" #'go-goto-docstring)
- (define-key m "f" #'go-goto-function)
- (define-key m "i" #'go-goto-imports)
- (define-key m "m" #'go-goto-method-receiver)
- (define-key m "n" #'go-goto-function-name)
- (define-key m "r" #'go-goto-return-values))
而 IDEA 也提供了一系列接口來實現(xiàn)類似的功能,如:
- gotoActionAliasMatcher
- gotoClassContributor
- gotoSymbolContributor
- gotoFileContributor
- gotoRelatedProvider
我們只需要分析光標符所在的位置,其所定義的語法,如 IDEA 里是 PSI,再實現(xiàn)對應的邏輯即可。如:
- @Override
- public @NotNull NavigationItem[] getItemsByName(String name, String pattern, Project project, boolean includeNonProjectItems) {
- List<CharjStructDeclaration> properties = findStructByKey(project, name);
- return properties.toArray(new NavigationItem[properties.size()]);
- }
這里定義的是數(shù)據(jù)結(jié)構(gòu)的導航。當我們按下快捷鍵的時候,會傳入 name、pattern 等信息。接著,從所有相關(guān)的文件(VirtualFile)中尋找對應的 struct,返回即可。
自動填充
主要可以分為兩類,一類是:代碼段(Snippets),一類是:自動填充(Completion)
好像也沒啥說的,就是綁定在特定關(guān)鍵字上的內(nèi)容。
其它
剩下的就是一些比較有意思的功能,諸如于:
- fileType。文件圖標支持。即某一類型的文件,使用特定的圖標來展示。
- commet 。即按下注釋的快捷鍵,能快速的注釋和反注釋代碼。
- line marker。IDEA 提供的功能,用于在行上通過圖標來展示特定的功能。
- folding。提供特定的代碼段的折疊功能。
- 數(shù)據(jù)視圖。展示特定數(shù)據(jù)結(jié)構(gòu)關(guān)系及參數(shù)等的視圖。
- ……
其它
我一直在尋找一直簡易的方式,以快速識別編程語言,并標識它們。所以,也就有了這篇文章。
雖然,還在探尋,但是呢,似乎已經(jīng)有了一個初步的結(jié)果。
本文轉(zhuǎn)載自微信公眾號「 phodal」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系 phodal公眾號。