Language Server Protocol

Language Server Protocol,

最近才注意到Language Server Protocol官方中文介紹)這東西,微軟為了 Visual Studio Code 所定的一個協定,專門用來輔助程式開發用的,像是 VSCode 的 IntelliSense 提供的自動補完就可以基於這個協定支援更多語言,這協定其實在 2016 就發表了,感覺我 lag 很久,不過其實我也好奇 VSCode 怎麼處理這問題一陣子了,最近在 TernJS 的 issue 裡面看到 LSP 這個詞,好奇之下才去看到底是什麼東西。

LSP 的設計理念是開發 Editor 的不可能每種程式語言都花時間心力去把它們的編輯輔助功能做起來(還不一定做的好),所以不如就把這塊拆出來,讓分析程式碼、提供輔助功能的部分(Language Server)拆出去給各自領域的人開發,然後透過一個公定的介面來做溝通,這個介面就是Language Server Protocol了。

LSP 是架構在 JSON-RPC 這個 protocol 上,只要你的 Editor 可以透過 JSON-RPC 發送請求並接收結果,就可以利用 LSP 來提供功能,現在支援 LSP 的編輯器也不少,不是只有 VSCode 有支援,其它還有 Eclipse、Vim、NeoVim、Sublime Text 3 都已經有方案可以支援了,在社群維護的網站langserver.org上有一份清單介紹各個 client 的支援狀況。

送到 Language Server 的指令,目前 Protocol 可以提供以下功能:

其它還有一些是檔案、工作區相關的操作指令,另外由於現在 Language Server 實做和 LSP 是分開的,也沒有限制一定要所有功能都有支援,所以有些 Language Server 可能是沒有支援特定功能的,目前可以找到兩份 Language Server 的列表,一份是 LSP官網的,另一份則是langserver.org上的,社群維護的版本才有標示不同的 Language Server 對應支援的功能,不過說是社群維護,其實 langserver.org 是另外一間公司Sourcegraph在維護的,該公司做的東西和 LSP 相關性看起來還蠻大的,也提供了很多 Language Server。

然後我就很感興趣,VSCode 現在內建的 JavaScript 用的 Language Server 是哪一套呢?畢竟仔細一看,兩個列表裡面,都沒有列出內建由微軟維護的 JavaScript 的 Language Server,只有 Sourcegraph 的版本,不止 JavaScript 沒有,TypeScript 也沒,只有TypeFox的版本(TypeFox 也是做程式碼相關工具的公司,我有找到一些研討會演講介紹 LSP 的講者就是這間公司的人)。總之兩個語言都沒列這真是太不尋常了,實在引起了我的好奇心,後來到處尋找總算在JavaScript in VS Code這頁找到蛛絲馬跡,這頁內文第二句話就有個連結連去JavaScript Language Service在 GitHub 的介紹,位置是 TypeScript 專案下的 Wiki 頁面,也有找到 TypeScript 專案內的相關程式碼,實際上 VSCode 對於 JavaScript 和 TypeScript 的編輯輔助功能都是依靠這個 TypeScript Language Service 提供的,或是也可以叫它tsserver,TypeScript 的大架構可以參考Architectural Overview這篇文章;由於 tsserver 比較早推出,所以用的不是 LSP 用的 JSON-RPC,而是 STDIO 然後傳輸 JSON 加上 header,指令也有些落差,不過其實整體而言沒差距很大,因為 VSCode 那些輔助功能幾乎都是從 Visual Studio 來的,TypeScript 的支援也早就都透過 tsserver 來實現了,事實上,Sourcegraph 的TypeScript Language Service就是個 tsserver 的 proxy,底層還是 tsserver,不過實際上要用的話應該是TypeFox 的比較好;然後當然也有人提出來說 TypeScript 是不是應該直接提供 LSP 版本的開發工具支援,在 GitHub 上的Issue 11274,不過目前看來是沒打算樣子,這點我也是蠻意外的,畢竟 LSP 和 TypeScript 同公司的,沒打算支援自家公司定的標準,也是十足的霸氣,也看的出來各開源專案自治度其實蠻高的。

補充:另外有個 debugger 用的Debug Adapter Protocol


Monokai Pro

Monokai Pro VSCode

因為用Dank Mono字體的關係,最近開始有想要讓 Vim 支援斜體的 syntax highlight,於是又花了不少時間測試,過程中想起在 Twitter 上看到有人說過有一款付費的 Sublime/VSCode 佈景主題(印象中是@yorkxin),叫Monokai Pro,因為可以免費評估,可能是用幾天後才會出現 popup 吧,就一時興起就裝來玩玩看,結果還蠻滿意的。

Monokai Pro Sublime

雖然我主力是 Vim,但是 Sublime 和 VSCode 都還是有用,後來又繼續研究了一下,原來 Monokai 是在Textmate 2的佈景主題,還蠻有名的,也很多人 port 到不同環境,Vim 那邊比較多人用的應該是molokai,然後 Monokai Pro 是同個作者做的,如果有 Vim 版的我會支持一下吧~

然後弄一弄發現我用 jellybean 的配色用到 256 色的,結果把 True Color 支援打開之後發現有點難看,又開始我的探索之旅了,目前暫時是用tender

tender.vim


Naming Cases

Camel,

整理一下各種多單字 identifier命名慣例(規則):

CamelCase

CamelCase 應該是最有名的了,單字的首字母大寫,其它字母小寫,然後其實還分為 UpperCamelCase 和 lowerCamelCase,UpperCamelCase 是指第一個單字的首字母大寫;lowerCamelCase 則相反,其中 UpperCamelCase 又稱為 Pascal Case,因為是 Pascal 語言當中常用的命名慣例,而因為有 PascalCase 這名稱代表 UpperCamelCase,所以也很多人直接用 camelCase 代表 lowerCamelCase;此外,也有 Dromedary Case 的講法,不過現在應該只要只剩下 Pascal Case 和 Camel Case 的說法比較有人用吧,Lower Camel Case 在 JavaScript Standard 裡面是命名變數用、Upper Camel Case 則是大部分語言推薦的建構函示和 Class 的命名慣例。

CamelCase 應該也是最早有名稱的,而且其實還有很多的別名,而除了 CamelCase 外,其它命名慣例都是有用個符號分隔單字,其中最常見到的就是 snake_case 了。

snake_case

snake_case 是用底線符號_做分隔,通常是全小寫,名稱應該由其外觀而來,是 Ruby 社群那邊出來的,應該可以算是象形文字的一個分支。在 Python 的 PEP 8 和perlstyle是用 snake_case 來命名 function。

MACRO_CASE

snake_case 的另一種形式是全大寫字母,因為 C 語言的 MACRO 使用,所以稱為 MACRO_CASE,偶爾有人稱之為 ALL_CAPS(不過其實全部大寫就可以稱為 ALL CAPS 了),也有一種說法叫 SCREAMING_SNAKE_CASE,通常是常數使用的命名慣例,另外像是 Bash 的環境變數、C 語言的 MACRO 等也是這個形式。

以底線為分隔的,在 perlstyle 裡面還有定義一種不常見的形式,首字母大寫加上底線分隔的 Some_Caps_Snake_Case,作為模組內的 global/static 變數,另外在 wikipedia 上有看到 Ada 語言也是用這種命名慣例,這種形式目前似乎沒有慣用的稱呼方式。

lisp-case

lisp-case 則是用連字號(hyphens)-做分隔,也一樣通常是全小寫,和 PascalCase 一樣因為程式語言 lisp 而得名,其實大部分語言都不支援 lisp-case,因為-同時是運算符號, parse 起來會蠻有問題的,除了 lisp 外我看過支援的還有 livescript,好像都還蠻偏 functional language 的,除了程式語言外,其實 URL 的路徑很常用,雖然主要是為了 SEO 效果,另外就是 HTML、XML 裡面的 attribute、id、class 也蠻容易見到用 lisp-case 的,而除了 lisp-case 這個名字外外,還有一個也很知名的稱呼是 kebab-case,和 snake_case 一樣是外觀而來的名稱。

COBOL-CASE

用連字號做分隔,但是全大寫的則是叫 COBOL-CASE,一樣是從 COBOL 語言而來。

Train-Case

以 hyphens 為分隔的,在 wikipedia 上還有看到首字母大寫的形式叫 Train-Case,不過沒有標註名稱出處,不多人用這個名稱,不過也沒其它名稱,以後應該也只有這個名稱吧,不常在程式語言內見到,Windows Power Shell 的指令是用這種規則命名的,另外一個比較常見的地方就是 HTTP Header 的 field name 了。

我自己其實是最喜歡 lisp-case,編寫 HTML 的時候 id、class 我都是用 lisp-case,次之是 snake_case,偏偏 JavaScript Standard 是用 camelCase 的,其實掙扎了一陣子,不過現在已經比較習慣一點了。

這些不同命名規則間的轉換其實有不少工具可以協助,Ian Storm Taylor 在 NPM 上有一整個系列的工具,支援很多種規則的轉換,還包括了書寫用的Title Case,講到這個就要提一下 CSS 裡面的text-transform的 capitalize,其實這個屬性只處理每個單字的第一個字母,也就是說,如果你本來是全大寫的 TITLE,用 capitalize 轉換後,還是 TITLE,如果要純 CSS 方案的,其它字母轉小寫,一個單字的話勉強可以配合::first-letter來辦到,不然就是輸出到 HTML 之前要先處理過,而且,capitalize 不是 Title Case,精確的 Title Case 是不會把一些介係詞、冠詞轉大寫的,例如「I Have an Apple」裡面的 an,這問題目前就是沒有 CSS 解法,有搜尋過一下發現,沒做的主因應該是因為 Title Case 幾乎只有英語用的上。

在 Vim 上如果要轉換一個變數名稱的命名規則,我是用switch.vim然後加上一組自訂的轉換設定:

let g:switch_custom_definitions =
    \ [
    \   {
    \     '\<\(\l\)\(\l\+\(\u\l\+\)\+\)\>': '\=toupper(submatch(1)) . submatch(2)',
    \     '\<\(\u\l\+\)\(\u\l\+\)\+\>': "\\=tolower(substitute(submatch(0), '\\(\\l\\)\\(\\u\\)', '\\1_\\2', 'g'))",
    \     '\<\(\l\+\)\(_\l\+\)\+\>': '\U\0',
    \     '\<\(\u\+\)\(_\u\+\)\+\>': "\\=tolower(substitute(submatch(0), '_', '-', 'g'))",
    \     '\<\(\l\+\)\(-\l\+\)\+\>': "\\=substitute(submatch(0), '-\\(\\l\\)', '\\u\\1', 'g')",
    \   }
    \ ]

這組設定是MACRO_CASElisp-casecamelCasePascalCasesnake_case這樣的順序循環切換,還蠻方便的,不用花大腦思考要轉成哪種規則然後下不同指令,就一直連打-就好。

其實一開始只是在想有多少種組合才開始查的,結果幾乎一般組合都有地方使用,只差符號分隔單字加 camelCase 的兩種形式吧,最後放一些參考連結:


Vim Packages

Vim 8 有兩個我覺得比較大的新功能,一是開始有Asynchronous I/O,二是開始有官方的 package 機制了,這篇主要想介紹這官方的 package 機制,眾所周知,以前 Vim 實在很難管理自己裝的 Vim script 和 plugin(後文以 plugin 為主),因為原始的設計是自己把檔案丟到runtime 目錄下的對應位置,裝的東西一多,就會開始混亂起來,最常發生的就是越來越多垃圾,不知道還需不需要用,再來就是可能會有檔名重複的情形,所以升級某個 plugin 遇到有檔名重複時,直接覆蓋過去可能也會出錯,這個問題直到 Tim Pope 推出pathogen.vim後才被解決,pathogen 是藉由修改runtimepath變數(有點像是系統的PATH環境變數,可以有多個路徑)來讓不同的 Vim plugin 可以放在各自的子目錄內,從此一舉解決了 Vim plugin 的管理問題,當然現在很多人用的Vundleneobundlevim-plug等,基礎原理應該都是一樣的。

Vim 8 推出的 package 機制,雖然其基本原理也是增加 runtimepath,不過它其實定位和 pathogen 不一樣,設計上是再高一個階層,不過也因此和 pathogen 的路徑設計不相容,pathogen 之類的都是把 plugin 分目錄放到~/.vim/bundle這,例如:

~/.vim/bundle/html5.vim
~/.vim/bundle/yajs.vim

然後會去把這些路徑加到runtimepath內(有些 plugin 是全自動、有些要設定、有些可以加條件),寫成 glob 型式大概是~/.vim/bundle/*,不過新的 package 定義上是數個 plugin 的組合,所以一個 package 下是可能有多個 plugin 的,放 package 的路徑一樣在~/.vim下面,預設在~/.vim/pack,也可以修改packpath來換位置,不過東西不是直接放進去就好了,一開始會被加進去 runtimepath 的路徑實際上是~/.vim/pack/*/start/*,在這個 glob 表示式中,第一個*是 package 層,第二個*則是 package 裡面的 plugins,例如我可以建立一個自己在編輯 JavaScript 時用的 plugin 組合,就先叫 my-js 好了,我就把東西都丟到~/.vim/pack/my-js/start/*,大概像是:

~/.vim/pack/my-js/start/yajs.vim
~/.vim/pack/my-js/start/javascript-libraries-syntax.vim
~/.vim/pack/my-js/start/simple-javascript-indenter

至於中間的start則是表示啟動就會去讀進來的意思,類似於以前 pathogen 的流程,而除了start之外,還有一個路徑是opt,是 optional 的意思,放在opt下面的 package 不會在啟動時就讀進來,而是要下packadd指令,例如packadd foo就會去找~/.vim/pack/*/opt/foo/這些位置有沒有東西可以用,文件上提供的一個使用情境是根據 Vim 版本決定要讀入哪一個 optional plugin,可以用 Vim script 做一些判斷來決定要讀那些,或是使用者自己執行 packadd,不過我思考一下是覺得後者的情境似乎不太有用,所以這個設計主要的目標應該還是做一些自動化判斷並讀入 plugin 為主吧。

當然,package 也可以只包一個 plugin,理論上可以直接這樣發佈 Vim plugin,不過現在這樣發佈,就會不相容於目前使用量最大的 pathogen 架構,所以我也還沒看過有人這樣直接發佈的,像 vim-css3-syntax 就還是用舊的資料匣架構,但是在 README 內加上對應 Vim package 的安裝方式,這是我目前覺的對於 Vim package 普及化的最大阻力;另外還有一個缺點是,如果完全用 Vim package 機制來裝 plugin,那其實也沒有地方紀錄你安裝了那些東西,和最早的時候,或是單純只有 pathogen 時一樣,要裝新機器什麼的就有點麻煩。目前我是覺得 Vim package 還不會很快普及,它比較像是出來取代 pathogen 的功能,應該接著要等有基於 Vim package 的 package manager 出來才會開始有普及的機會吧。


Vim Filename Complete

Vim Filename Complete,

Vim 有一個內建的自動補完功能是針對檔案名稱的,使用的方法是<C-X><C-F>,我目前在維護的autocomplpop也有支援這種補完模式,只要輸入./後就會自動幫忙觸發,不過我比較有機會觸發到是在使用 ECMAScript 6 的 import 和 CSS 的 import 時,不過常常就是發現他查看的路徑不太對,不是拿目前編輯檔案的位置做為起點的,研究過後發現是因為 Vim 找檔案的起點是看他的工作目錄($PWD),加上我會使用ctrlp這種工具,所以實際上在編輯的檔案通常是不在工作目錄下,對於這個問題,其實我覺得最理想的解決方式是 Vim 應該要提供兩種模式來決定要從那邊開始找,不過目前似乎沒這個計畫,唯一在文件是有提到的是未來可能會支援path的設定,理論上,如果有支援的話,應該就可以解決問題了,因為預設的path值包括了.,不過目前還沒有相關時程,就只能自救了。

最簡單的方法,其實就是開啟autochdir,這個選項打開後就會自動在切換 window 時也更改工作目錄,不過這個選項是為了相容早期系統才提供的,文件也有說可能會和部分 Vim Script 不相容,實際上我也有找到一些不相容的 Vim Script,所以想避免,就搜尋了一下其它可能的解決方法,在 StackOverflow 上有看到一篇,裡面有兩個人提供了解法,第一個是用autocmd,然後在進入 insert mode (在這時候才有機會用到檔名補完的功能)時自動開啟autochdir,離開時自動關閉autochdir,不過這樣的方式(感覺上)還是不太安全,因為還是用到autochdir,所以下面有另外一個方法改用lcd,作法是改成修改 Key Mapping 的方式,改的 mapping 是./<C-X><C-F>,不過這樣對我來說又不合用,因為我用 autocomplpop 的話,不會真的打<C-X><C-F>,所以基本上觸發不到這事件,所以我就決定把這兩種解法合併起來,改成用autocmd加上lcd

:autocmd InsertEnter * let save_cwd = getcwd() | execute 'lcd %:p:h'
:autocmd InsertLeave * execute 'lcd' fnameescape(save_cwd)

進入 insert mode 時改變該 window 的工作目錄,離開 insert mode 時把工作目錄還原。這是我目前認為影響最小的調整方式,不過其實可能執行一次lcd換工作目錄就夠了,沒深入研究 autochdir 所產生的問題,不過我推測是影響到 Vim Script 建立的 window 的工作目錄,像是 NERD Tree 之類的側邊欄那種,總之目前這樣運作還算正常,接下來就是等 Vim 加上path的支援吧(或是有人送 patch)。


Native True Color Vim

因為最近 Vim 8 發佈了,所以就又研究一下現在最新的 True Color Vim 安裝方法,結果發現已經併進 master branch 許久了,然後從7.4.1784開始,也不用加特別參數來編譯,只要--with-features的值是big或是更大的huge就會把這功能編譯進去,所以現在就不用 ZyX 維護的版本了,目前用的編譯指令為:

git clone https://github.com/vim/vim.git

cd vim
cd src && make autoconf && cd ..

./configure \
  --enable-gui=no \
  --without-x \
  --enable-multibyte \
  --with-tlib=ncurses \
  --enable-cscope \
  --with-features=huge \
  --disable-nls \
  --enable-perlinterp \
  --enable-pythoninterp \
  --enable-rubyinterp

make
make install

然後現在也不需要guicolors的設定,好像直接就生效了,顏色畫出來和之前的 ZyX 的版本似乎有一點差異,我想應該現在新的版本是比較正確才是。追蹤這功能追了這麼久,總算也是告一段落了,感覺...好像也沒什麼特別的感覺...


YAJS.vim and Vim Syntax Highlight

上個週末在香港 Open Source Conference分享的主題,第一次使用英文分享,結果表現不太好,不知道上場前喝點啤酒會不會比較好就是...

這個主題本來是想要投 COSCUP 的,主要是想介紹一下之前在做yajs.vim時遇到的比較有印象的問題,在這之前先介紹一下 Vim Syntax Highlight 的機制,因為這些問題很多都和 Vim 的 Syntax Highlight 機制的設計關係很大,然後最後就是有一個還沒辦法解的問題,這個問題就是 yajs.vim 目前還沒辦法完美的 highlight 有 default parameter 的 arrow function。


Vim Syntax and Regexp Note

前陣子為了寫更好的 Vim syntax 還去學了 compiler 的課程,雖然沒上完不過也對怎麼解析語法理解不少,不過其實 Vim syntax highlight 系統為了效能問題,有不少限制,沒辦法真的和 compiler 的 parse 原理完全互通,其中兩個限制影響比較大,第一個是沒辦法有完整的 AST 並解析其語意,因此除非寫得非常繁複,一定會有無法正確 highlight 的地方,例如 comment,不是說 comment 不能正確標示,問題是 comment 可以插入在很多地方,像是參數序列的中間,function關鍵字和後面()的中間等等,幾乎是可以放空白字元的地方就可以放 comment,然後不會影響程式語意,本來,不考慮註解時,我可以用skipwhiteskipempty然後加上nextgroup就可以指定下一個 token 是什麼,以 JavaScript function declaration 來說:

function fn (a) {}

這樣的程式碼我把他拆成四個部分,function keyword、function name、function parameter、function body,然後用 Vim syntax 語法設定:

syntax keyword javascriptFuncKeyword function nextgroup=javascriptFuncName skipwhite
syntax match   javascriptFuncName    contained /\k\+/ nextgroup=javascriptFuncParam skipwhite
syntax match   javascriptFuncParam   contained /([^()]*)/ nextgroup=javascriptFuncBody skipwhite
syntax region  javascriptFuncBody    contained start=/{/ end=/}/

除了 function keyword 外都有contained,用途是讓該 rule 不會在 TOP region 下生效,一個好處是減少 TOP region 下要檢查的 rule 數量,另一個好處是有些相同的 token pattern,但是其實語意上是不一樣的,可以盡量用這種機制拆分開來,到這裡都還很美好,但是加上 comment 後問題就變複雜了,先簡單寫一下 comment 的 syntax rule:

syntax region  javascriptComment     start=/\/\*/ end=/\*\//

然後 comment 可以放在哪些地方呢:

function /*cc*/ fn (a) {}
function fn /*cc*/ (a) {}
function fn (a) /*cc*/ {}

也就是本來 nextgroup 連接的地方都可以插入個 comment,可是只要插入了 comment,後面的 token 就不會被正確 highlight,因為 comment 的 rule 沒有 nextgroup,所以他的部分結束後就會回到用 TOP region 的情境,而後面應該符合的 rule 都有設上contained,所以就沒機會對到。當然現在要解決這個問題也不是沒方法可以避開,但是非常不好看,就是如下的設計:

syntax keyword javascriptFuncKeyword function nextgroup=javascriptFuncName,comment1 skipwhite
syntax match   javascriptFuncName    contained /\k\+/ nextgroup=javascriptFuncParam,comment2 skipwhite
syntax match   javascriptFuncParam   contained /([^()]*)/ nextgroup=javascriptFuncBody,comment3 skipwhite
syntax region  javascriptFuncBody    contained start=/{/ end=/}/

syntax region  comment1     start=/\/\*/ end=/\*\// nextgroup=javascriptFuncName,comment1 skipwhite
syntax region  comment2     start=/\/\*/ end=/\*\// nextgroup=javascriptFuncParam,comment2 skipwhite
syntax region  comment3     start=/\/\*/ end=/\*\// nextgroup=javascriptFuncBody,comment3 skipwhite

如此可以確保 comment 插入也不會讓後面的 token 沒 highlight,但是這樣的設計,實際寫起來會非常繁瑣,完全不想去研究 JavaScript 中會有多少類似的狀況。其實我是覺得 Vim syntax 應該是希望盡量都用前後獨立的 rule 來 highlight,盡量不要有前後相依的關係存在,就不會有上面的問題,也可以讓 highlight 過程比較單純,理想上是從頭開始,一個 token 一個 token 各自獨立的 highlight,不過是事情當然沒這麼簡單,第二個想記錄下來的事情也和這個有關係。

假設目前 highlight 處理中,parse 到一個=,然後看到一組小括號(a),連起來如下:

= (a)

這時你會覺得(a)是什麼呢?是小括號,裡面是一個 expression 然後回傳變數a嗎?相信很多人會這樣認為,如果他後面是接分號的話:

= (a);

但是其實也可能是這樣子的:

= (a) => {}

ES6 的 arrow function,也就是說,如果一個 token 一個 token 解析,一定無法直接知道目前 token 代表的正確意義,所以 compiler 把程式碼轉成 AST 的時候,有時候會先往後面看一下來判斷現在的 token 到底是什麼意義。然而 Vim syntax 系統並沒有這種能力,嚴格來說,是可以用 match 來達成,不過還是很受限制。再來則是往前看的問題,我在設定運算子的 match rule 的時候,會希望嚴謹一點,本來想在兩邊加上 word boundary 的 pattern,在 Vim 裡面是\<\>,不過測試幾回發現,我的字元本身不是文字字元的話,這個 pattern 是沒有用的:

/\<word

這樣是有效的,但是下面想要 match==的會沒用:

/\<==

所以變成要自己寫往前看的 regexp,在 Vim 裡面有兩種類似的東西可以用,分別是\zs\@<=,通常,\zs效能比較好,會推薦使用,他的用途是標註你的 regexp 的 match 的起點,當然同時也有一個\ze是終點:

/abcd\zsefgh\zeijkl

上面這串 regexp 的目標是efgh,但是他的前後分別是abcdijkl,實際執行時會去找abcdefghijkl這串字串,完整比對到之後,只會回中間的efgh作為 match 的範圍,這設計要做一些操作的時候就會有差,像是文字取代。本來我就想要用這個來做 syntax,可是就發現還是不生效,所以改成用\@<=試試看:

/\(abcd\)\@<=efgh\@=\(ijkl\)

就發現成功了,想了許久才理解其原因,然後才瞭解,真的往前看的是\@<=\zs並沒有往前看,兩者最大的差異在於 pattern match 操作的起點,一般的使用大概感覺不太到差異,不過像是 syntax highlight 這種一個 token 一個 token 逐步處理的就會有差,當目標是efgh時,通常處理進度到e這個位置時,前面的abcd已經被處理過了,所以這時候會和 regexp 比對的字串就變成efghijkl,使用\zs的話,因為它還是要完整比對到abcdefghijkl,起點是a,就不會和efghijkl相符,但是用\@<=的話,pattern 的起點是efghe,這樣就可以 match 到目前剩餘的字串了。

前面說的個 token 一個 token 逐步處理的問題還有一個情形也讓我困擾很久,不過這次不是東西被用掉,問題是沒被用掉。這個狀況發生在巢狀結構的 region,像是 JavaScript 的 block:

syntax region  javascriptBlock start=/{/ end=/}/

然號要讓 block 裡面可已有 block 就要用contains

syntax region  javascriptBlock start=/{/ end=/}/ contains=javascriptBlock

這時候就會發生奇怪的現象了,因為外面的 region 包括了頭尾的括號,然後進入 block 內要做 syntax match 的時候,一開始的{又 match 到 block,結果 Vim 就直接覺得這已經是第二層的 block 了,雖然好像有其它機制讓他不會一直循環下去變成無限多層,不過這樣還是會造成後面的 code 有被判斷錯誤的機會,因為 block 的開關不 match,這裡的關鍵也是要讓{}被處理掉,進入 region 內部就不會跑到上一層的起點,而這裡要用的就是matchgroup

syntax region  javascriptBlock matchgroup=bracks start=/{/ end=/}/ contains=javascriptBlock

如此就都會正常了,因為這樣的設定會讓{}直接被當成bracks這個 group,然後就被當成已經被解析過的 token,從它的下一個 token 繼續 highlight 分析,但是千萬不要另外加上bracks的 syntax rule,剛好又 match 到 region 的起點和終點:

syntax match   bracks  /[{}]/

這樣的話也會發生其它的怪異現象,總之 nested region 的重點在於,要用 matchgroup,然後不要用和 matchgrouop 同樣名稱,同樣 pattern 另外又設定一組 rule。

最後一個要紀錄的則是 Vim syntax 裡面的優先度,基本上是 keyoword 優先度最高,也就是有 match 到 keyword 的話,你的 match pattern 就都無效了,所以像是 JavaScript 裡面,label 雖然不可以用關鍵字,像是continue:就不合法,但是因為會先 match 到continue關鍵字,所以就很難用 syntax highlight 來標出這種錯誤,而在 keyword 比對完之後,才輪的到 region 和 match,兩者是同樣權重,但是後定義的優先,而且不受containsnextgroup裡面的順序影響,搞清楚優先順序在做細部的 syntax highlight 的時候還蠻重要的。另外要順便說說 region contains 和 nextgroup 的差異,nextgroup 其實還蠻不錯的,他不是限制下一個 token 一定是哪些東西,而是改變優先順序,先檢查完 nextgroup 裡面的東西,再檢查該 region 下的其它可能性,region contains 就不一樣了,該 region 裡面只有在 contains 裡面的東西會出現,另外還要特別注意一點,region 的處理並不管該 region 能不能正確的關閉,只要 match 到起點,就會把 region 打開,然後剛剛有提到,region 和 match 是同樣權重的所以就要非常注意:

syntax match  javascriptLabel       /\k\+:/
syntax region javascriptLabelblock  start=/\k\+: {/ end=/}/

這樣兩條 syntax highlight rule 然後配上下面的程式碼:

abcd: {
  var ii = 1 + 1;

var jj = 2 + 2;

要注意我的 block 其實沒有結束,但是結果 Vim 只會 match 到 label block 那條規則,而且由於一直找不到 region 的結束點,所以下面的var jj那行也是被認為在 block 內。

最後的最後要推薦一下gerw/vim-HiLinkTrace這個 Vim plugin,可以很完整的 trace syntax highlight 的狀況。


Latest True Color Vim

上次文章的安裝方法已經過時了,更新一下:

hg clone https://bitbucket.org/ZyX_I/vim
cd vim
cd src && make autoconf && cd ..

./configure \
  --enable-gui=no \
  --without-x \
  --enable-multibyte \
  --with-tlib=ncurses \
  --enable-cscope \
  --with-features=huge \
  --disable-nls \
  --enable-perlinterp \
  --enable-pythoninterp \
  --enable-rubyinterp \
  --enable-termtruecolor

make
make install

其實就是現在 ZyX 自己維護的 git repo 已經是有把 True Color 支援的 code merge 進來的版本,只是現在的最新版設定上有些修改,差異在不一定會直接輸出 true color 的色碼 sequence 了,要 Terminal Type 是xterm的時候才有。如果不是xterm的時候則需要加上設定檔,告訴 Vim 要怎樣輸出色碼的 sequence:

let &t_8f="^[[38;2;%lu;%lu;%lum"
let &t_8b="^[[48;2;%lu;%lu;%lum"

其中的^[是控制碼,就像是 BBS 著色一樣,不能直接打,Vim 的話要用 Ctrl + v + [ 三個按鍵同時按來輸入。這兩個設定項目因為是 ZyX 版才有,所以如果是跑普通的 Vim 的話,啟動時會跑出警告,可以用:help xterm-true-color看看比較詳細的說明。

最後 iTerm2 雖然已經出 2.0 了,不過 True Color 的正式支援是放在 3.0,所以目前要在 Mac 上用 True Color Terminal 還是需要抓 nightly 版的 iTerm2。


JavaScript Parameter Complete

前陣子在 FB 上說過突然有想做的 Vim Plugin 已經開發的差不多,所以把 1.0 放上了,這個又是自動補完的 plugin,架構設計上是沒針對 JavaScript,不過目前我只有針對 JavaScript 做好補完選項,這個 Plugin 一開始是為了處理Web Crypto API,這組 API 在做加解密的時候是需要指定演算法,不過演算法的名稱裡面,有一些有-在裡面,像是SHA-256之類的,以前這種字串的關鍵字,還可以加到 syntax 裡面,然後用 syntax complete 來處理,不過-不是 JavaScript 的關鍵字,會被忽略掉,為了處理這個問題,就開發了這個 complete function,簡稱jspc.vim

這個 complete function 的功能自動補完各種函數內的字串參數,像是剛剛提到的 crypto algorithm,還有各種事件名稱,Media Type 等,其實越做發現越多這類的選項,目前支援的東西包括:

  • Web Crypto Algorithm Name
  • Event Name for DOM, jQuery, Backbone
  • HTTP Methods for XHR, fetch
  • HTTP Headers, some values
  • Locales for Intl
  • HTML Tag Name for DOM API
  • Image Type for canvas.toBlob, canvas.toDataURL

這個 function 的原理還蠻簡單的,如果抓到游標位置看起來在字串內(以下用底線代表游標位置):

n.addEventListener('cl_

然後就會往前找看看這是不是一個 function call 的內部,是的話去看看 function name,像是上面的範例就會找addEventListener,然後就去內部先定義好的表單找看有沒有候選的項目,有的話就會根據字串內容去做過濾後然後給回來。

如果前面有其他參數是不會受影響的,不過 function name 和字串要在同一行,然後除了 JavaScript 的語法用(做為判斷是不是 function call 之外,其實還有支援 CoffeeScript 那種只用空白接餐處的寫法:

n.addEventListener 'cl_

其實判斷方法很簡單,就是找空白前面不是,的話就是了,一時還想不到是不是有其他種語言的 function call 的語法跳脫這兩種寫法的,我想應該可以支援大部分的程式語言了,不過裡面的候選資料還需要人來做苦工,所以我只弄了 JavaScript 的 Web 相關的東西,然後同時給 CoffeeScript、LiveScript、TypeScript 用,安裝方法就照用 Pathogen 那套裝法就可以了裝起來直接用,因為實做上是把原本的 omni complete function 包起來,所以不用特別做什麼設定,就可以和其它自動補完的輔助工具合作,像是vim-autocomplpopneocomplcache.vim,其實本來是獨立一個 function 的,但是這樣要和 Neo Complete Cache 整合太難了,乾脆就走 Vim 的 omnicomplete 路線,結果意外的方便,最後就是有什麼問題歡迎提出摟~


此類別所有文章